-
Notifications
You must be signed in to change notification settings - Fork 0
Authentication and pairing
Two credentials, never one: an enrolment key to begin pairing, and a per-device credential to actually use the API. This is the mechanism behind the pairing prompt in the panel and the app.
Breeze Core uses an RFC 8628-style device-pairing flow, so no long-lived shared password rides on every request:
- The
api_keyis an enrollment secret: on its own it only authorizes starting a pairing. - A client
POSTs/api/auth/enroll/start(with the key) and shows a short, single-use code (~60 s). - An admin on the LAN approves that code (via
tools/ac-approve.zshorPOST /api/auth/enroll/approve). - The client polls
/api/auth/enroll/polland is bound to a per-device credential — individually named, revocable, and expiring. - Control endpoints require the API key and a valid device credential. Approval and device management are admin-only and restricted to the local network.
Rotating the api_key doesn't log devices out (device credentials are
independent); revoke a single lost device with
ac-approve.zsh revoke <token_id>.
A device is pinned to the credential profile it enrolled with. The server
advertises which it supports in GET /api/version
(auth_versions, min_auth_version).
v2 — Ed25519 request signing (current, recommended). The client generates
an Ed25519 keypair; the private key never leaves the device and the server
stores only the public key — so a devices.json leak yields nothing an
attacker can use. Each request is signed over a canonical string binding the
method, path, timestamp, a single-use nonce, and a SHA3-512 digest of the
body, sent as headers:
X-Breeze-Auth-Version: 2
X-Breeze-Key-Id: <token_id> # names the device (not secret)
X-Breeze-Timestamp: <unix seconds> # must be within ±60 s of the server
X-Breeze-Nonce: <base64url, 16 bytes> # single-use within the window
X-Breeze-Signature: <base64url Ed25519 sig>
# signed message =
# "breeze-auth-v2\n{METHOD}\n{path?query}\n{timestamp}\n{nonce}\n{sha3_512(body) hex}"
The timestamp + nonce give replay protection; the body digest gives tamper
protection. At enrollment (auth_version: 2) the client sends its
public_key in enroll/start; nothing secret is ever returned by poll.
v1 — bearer token (legacy). Authorization: Bearer <token>; the token is
256-bit random, shown once at enrollment, and stored only as a SHA-256 hash.
Still fully supported.
AC_MIN_AUTH_VERSION (default 1) is the clamp. At 1, both v1 and v2
devices work — a new device may still enrol as either (the web UI and the
zsh/binary CLIs are v1-only, so this must stay open by default), and
v1-authenticated responses carry an advisory X-Breeze-Upgrade: auth-version=2 header. Raise it to 2 once your clients are updated and v1
is fully closed:
- v1 control requests are refused with
426 Upgrade Required+ a human-readable message (which even an un-updated client surfaces). - v1 enrollment is refused too —
enroll/startwithauth_versionbelow the floor returns426, so no new legacy credential is ever minted (not even a dead one that would be clamped on its first call).
So a clamped server effectively runs v2-only: existing v1 devices must upgrade, and no new v1 device can be created. Migrate the web UI and CLIs before raising the clamp — they can't do v2 yet.
An enrolled v1 device upgrades to v2 in place — POST /api/auth/upgrade,
authenticated by its existing credential, registers a freshly-generated
public key and keeps the same token_id. No re-pairing and no admin
re-approval (it re-keys a device that already proved possession, trusting
nobody new). The Breeze app does this automatically on first launch after an
update.
The bundled web UI and the diagnostic CLIs remain v1 clients for now; they keep working while
AC_MIN_AUTH_VERSION=1(the default). Migrate them before raising the clamp to2.
Every rejection carries a machine-readable reason, because "401" alone can't tell a client whether to fix the request or throw its credential away. Getting that wrong is expensive: an app that re-pairs on any 401 will discard a perfectly good key the moment a phone's clock drifts, and then need an admin on the LAN to approve it again.
{"detail": {"error": "unauthorized", "detail": "Request timestamp outside the allowed window",
"reason": "clock_skew", "retryable": true,
"server_time": 1755284412.51, "client_time": 1755284100.0,
"max_skew_seconds": 60}}reason |
retryable |
What the client should do |
|---|---|---|
no_credential |
no | nothing was presented — enrol |
unknown_key |
no | the server has never heard of this key id (revoked, or a wiped devices.json) — re-enrol |
expired |
no | past AC_TOKEN_TTL_DAYS — re-enrol |
bad_signature |
no | the signature doesn't verify for this key — re-enrol |
bad_api_key |
no | the shared enrollment key is wrong — fix the key, don't touch the device credential |
clock_skew |
yes | the credential is fine; the timestamp isn't. server_time is included so the client can learn its offset and retry — the Breeze app does exactly this |
replay |
yes | this nonce was already used; sign a new request |
incomplete_signature |
yes | a required signing header was missing or malformed |
Retryable reasons must not cost you your credential. Both retryable and
definitive failures are logged server-side with the reason, the client IP, and
a key hint (never the secret) — journalctl -u breeze-core -t meow-ac.auth.
| Client | Credential |
|---|---|
| Breeze for Android | Ed25519 since app 2.0 / server 3.0.0. Seed in the Android Keystore. |
| The web panel | Ed25519 from 3.2.0. Non-extractable CryptoKey in IndexedDB — the page cannot read its own private key. Falls back to a bearer token on a browser without Ed25519. |
breeze-core diag |
Bearer token, self-enrolled and cached. |
| Anything you write | Either. AC_MIN_AUTH_VERSION=2 refuses v1. |
Before the panel could sign, setting AC_MIN_AUTH_VERSION=2 locked the panel out
along with everything else unsigned — so in practice the clamp was unusable for
anyone who used the browser. It is usable now.
Whether a browser can do this at all is worth checking before relying on it: the package host serves a one-page capability probe.
Breeze Core · Breeze for Android · Packages · AGPL-3.0
Start here
Install it
Use it
Reference
Run it safely
Develop and port