Skip to content

Security

monikapurpl3 edited this page Aug 21, 2026 · 1 revision

Security

  • Ed25519 request signing (Breeze Core ≥ 3.0.0). The private key is generated on the device and never leaves it — a seed in flutter_secure_storage, backed by the Android Keystore — and the server holds only the public key. Every request is signed over method + path + timestamp + single-use nonce + SHA3-512 body digest: no replay, no tampering, and a server compromise exposes nothing forgeable. A device on the old scheme upgrades itself in place on first launch against a 3.0 server.
  • Bearer fallback for pre-3.0 servers, also Keystore-backed. A 426 triggers the in-place upgrade.
  • allowBackup=false keeps credentials out of device and cloud backups.
  • HTTPS enforced for non-private hosts — Android blocks cleartext by default, and the app does not make an exception for anything outside your LAN.
  • Admin approval is LAN-only, enforced by the server. The app never performs approval; it only asks for a code and waits.

How a 401 is handled, and why it matters

This was the app's worst bug, fixed in 2.1.1, and the reasoning is worth keeping visible.

The app used to treat any 401 as "my credential is dead": it deleted the stored Ed25519 private key and demanded re-pairing. Re-pairing needs an admin on the LAN — so a user away from home could not recover, and when the immediate re-enrolment also failed, the app fell through to onboarding having forgotten the server entirely.

The trigger did not even have to be real. A phone whose clock had drifted past the server's 60-second window fails every signed request, which looks identical to a revoked credential.

Now:

  • Transient failures are retried once, transparently. On a clock_skew rejection the app learns server_time − device_time and signs everything afterwards with server time, so a drifted clock self-heals in one round-trip instead of costing a pairing.
  • Re-pairing only happens when the server says the credential is genuinely finished, or after three unexplained 401s in a row for servers too old to say which it is.
  • On a kept-credential 401 the app drops to its offline state instead of continuing to poll every 5 seconds. That hammering is what tripped the server's fail2ban and, behind NAT, banned everyone's shared address at once.

The server half of this — stable error codes and a retryable flag — is in Authentication and pairing.

Reporting a vulnerability

Privately, please: SECURITY.md.

Clone this wiki locally