docs(auth): what building the register's side settled

ADR 0017 records the decisions taken in building (SRP's identity, the
decoys, the 72-hour account reset proposed for confirmation, GitHub's
deprecation) and what step 3 must do; the module's README and the
AGENTS files say how password sign-in works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-20 13:32:21 +02:00
commit c67b492bfd
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -84,6 +84,66 @@ that the instance doesn't also know, so nothing could be encrypted for the perso
key (5); passwords taken as RFC 8265's OpaqueString (other spaces to a plain space, NFC,
control characters refused) before Argon2id, so a password unlocks alike on every system.
## Settled in building the register's side (20 Oct 2026)
In `halkyon_base_server` (its README has the details); members forward every step
through `AccountRegister`, the session being theirs.
- **Sign-up** (`password.set`) needs an address: the person's own, or one a mailed code
proves in the same call, which makes an anonymous person identified under their ID.
The password signs in only once an authenticator is confirmed; until then a new setup
replaces it. The register checks what it can: salt 16 bytes, Argon2id `argon2id13` at
3–16 passes and 64 MiB–1 GiB, the verifier 1 < v < N padded to N, public keys 32 bytes,
each wrapping a 24-byte nonce and 112 bytes.
- **SRP's identity I is the sign-in's challenge ID**, not the account's: x comes from
Argon2id over the password and salt, so I only binds the proof to this sign-in, and
an account ID handed to whoever types an address would tell who has one.
- **Addresses nobody has** (or whose account has no password) get a salt and verifier
from a third instance key, `signInDecoyKey`, and the address's hash, the same queries
and modular arithmetic (both are computed for every sign-in), a challenge row of their
own, and in the second step the same refusal. One refusal, `wrongCredentials`, for a
wrong password, an unknown address, and a wrong, used or missing code;
`setupIncomplete` (no confirmed authenticator) and `notDue` (a reset waiting) only
after a right password. Begun sign-ins are limited to 10 an hour per address and 30
per network; challenges last two minutes and are taken once.
- **TOTP secrets** are sealed with XChaCha20-Poly1305 (pub.dev's `cryptography`, pure
Dart, pinned) under a key derived from `totpKey`, bound to the row's account and ID;
each step's code passes once (a conditional update of the last step). At most 10
authenticators; one not confirmed within a day is pruned; the last confirmed stays;
removing one, or new recovery codes, takes a code (10 tries an hour per account).
- **Resets** (D3): an authenticator reset takes effect 72 hours after the password and
a mailed code or the recovery phrase ask, with a mail to the address at once (none
sent, none started); a sign-in with a code calls off every reset waiting. Once due,
the first sign-in with the password alone carries it out (authenticators and recovery
codes go), and the password alone signs in until a new authenticator is confirmed.
The recovery phrase is proved by the account's Ed25519 signature over
`halkyon key proof v1|`, the challenge's ID and a nonce. A password set with the
phrase, or an account reset, ends the account's sessions on the register's server.
- **The account reset without the phrase waits 72 hours too, with a mail at the start**
(a mailed code asks, a new one carries it out once due; a sign-in with the password
calls it off): a proposal, for the developer to confirm. Carried out: a new password
and new keys, no authenticators.
- **Keys:** without all three keys (or without email sign-in) the instance offers no
password sign-in (`passwordSignIn` in its basics), so an instance can take these
releases before its operator makes keys; a key set but shorter than 32 characters
stops the server.
- **GitHub** (D2): an account with a password and a confirmed authenticator is refused
(`usePassword`), one without signs in as before and the app leads to setting up an
address, a password and an authenticator (its ways in say what is missing): a refusal
there would lock out GitHub's people on a new device, as nothing else signs them in.
Where passwords are offered, no new GitHub account is linked (`deprecated`). From
`GITHUB_SIGN_IN_ENDS` (a day the operator sets) GitHub signs nobody in (`ended`), the
basics stop offering it, and the daily pruning forgets every GitHub ID.
- **Email codes** still sign in (until step 3), but not into an account with a password
and a confirmed authenticator (`usePassword`), which a code alone would bypass.
Step 3 (email codes go): sign-up keeps `password.set` with its code; `emailCode.verifyCode`
and `emailSignIn.signIn` go, and with them the switch token; accounts with an address but
no password, and accounts whose setup never finished, need a way in (a mailed code and a
new password, as the account reset, or a password plus a mailed code until an
authenticator is confirmed); a member's sessions should end too when the register resets
an account; and a signed-in change of password (with the keys at hand) is still missing.
## Consequences
- People keep a password, an authenticator app and a recovery phrase: more to keep than a