docs(auth): what the second reading found, and what was done (ADR 0017)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
d53844bc93
commit
1908e4f362
1 changed files with 65 additions and 1 deletions
|
|
@ -113,7 +113,8 @@ through `AccountRegister`, the session being theirs.
|
|||
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.
|
||||
per network (per address and network since the second reading, below); challenges
|
||||
last two minutes and are taken once.
|
||||
- **Per-network limits count by the visitor's address** (`networkOf`): behind a
|
||||
reverse proxy every connection is the proxy's, so for a connection from a proxy the
|
||||
operator trusts (`TRUSTED_PROXIES`, Caddy's fixed address in `deploy/compose.yaml`) the
|
||||
|
|
@ -227,6 +228,69 @@ released, stays at 1), after the second reading.
|
|||
C compiler and make for sodium's build hook (Linux, Android's NDK, Xcode, Visual
|
||||
Studio; the snap's build and CI's runner too).
|
||||
|
||||
## Second reading (20 Oct 2026)
|
||||
|
||||
An independent reading of everything above (`f6dfbbe..` the branch) found nothing
|
||||
critical; what it found, and what was done (the developer decided M3, M4, M8 and
|
||||
deletion):
|
||||
|
||||
- **H1, high: the phrase's proof was the account's own key.** A key proof was a
|
||||
signature by the account's signing key, which every signed-in device keeps: a readable
|
||||
keyring and one TOTP code set a new password, without the password or the phrase. The
|
||||
phrase now proves itself with an Ed25519 key derived from its bits alone (halkyon_keys'
|
||||
`recoveryProofKeysOf`, KDF context `hlkrprf1`), its public half sent with every setup,
|
||||
new phrase and account reset and kept as `account_password.recoveryProofKey`
|
||||
(nullable: an account without one proves no phrase until it sets a new one); the device
|
||||
derives the secret half only while the phrase is typed.
|
||||
- **M1: decoy keys were random bytes**, a real X25519 key one time in sixteen, so an
|
||||
address with an account could be told apart; decoys' keys are now made from seeds.
|
||||
- **M2: the password alone, once an authenticator reset took effect, left an account
|
||||
reset waiting**, which the mailbox's holder could carry out after the owner was back;
|
||||
every sign-in with the password now calls it off.
|
||||
- **M3: anyone knowing an address could spend its hour.** Sign-ins begun count per
|
||||
address and network (10), phrase proofs likewise (5), networks as before (30); failed
|
||||
proofs count per address from every network, 30 an hour, past which nothing begins for
|
||||
it (so many networks together make 30 guesses an hour, and holding an owner out takes
|
||||
failing without end from at least three; longer windows would outlive the hourly
|
||||
pruning of the limits' notes); changes begun, 10 an hour per account. Codes by mail:
|
||||
3 an hour per address and network, 10 per address from every network, 10 per network,
|
||||
and up to 3 valid at once, a fourth voiding the oldest.
|
||||
- **M4: codes could be guessed forever.** Codes refused after a right password (or from
|
||||
a signed-in device) count per account; after every tenth in a row no code is taken
|
||||
(`codesWait`), an hour, then twice as long, a day at the most, the first wait mailed
|
||||
to the address where the device sent it (sign-in and the phrase's reset; the instance
|
||||
keeps none). At a day, ten guesses a day keep about a 1 % chance a year against one
|
||||
authenticator: the mail is what tells its owner to change the password.
|
||||
- **M5: IPv6 visitors.** Counted by their /64; and Caddy's network gets IPv6 (a unique
|
||||
local /64, Caddy's address in it trusted too), as Docker's userland proxy showed every
|
||||
IPv6 visitor as the gateway (reasoned, to be checked on the server: NEXT.md), in a
|
||||
commit of its own, as it needs the network made anew.
|
||||
- **M6: nothing bound a proof to the instance.** SRP's identity is now a label, the
|
||||
challenge's ID and the host the device addressed (`passwordIdentity`), and the phrase's
|
||||
proof signs that host, its purpose and the new password's digest (key proof v2); in
|
||||
production the host must be the server's `apiServer.publicHost` (`otherInstance`
|
||||
otherwise), any in development; members forward their own. A hostile instance relaying
|
||||
a sign-in or a phrase's proof to the real one, or swapping in a password, gets nothing.
|
||||
- **M7: data keys weren't authenticated.** A sealed data key is now signed by its sealer
|
||||
and checked against the expected one on opening, so the register can't plant a key of
|
||||
its own (before step 4 encrypts anything).
|
||||
- **M8: passwords had no minimum.** New ones have at least 12 characters (code points,
|
||||
once normalised), checked on the device; signing in takes any.
|
||||
- **Deletion:** an account with a password deletes itself only with its proof and a code
|
||||
(`DeletionProof`), as a change takes; others with their session, as before.
|
||||
- **Lows:** a member waits at most 5 seconds for the register's moment after a reset
|
||||
(`maxClockSkew`), refusing beyond (clocks must agree by NTP; a member's clock ahead by
|
||||
less leaves sessions made in that gap); `instanceSecret` stops the server when shorter
|
||||
than 32 characters; a change of password, a reset keeping the keys and a new phrase
|
||||
write their own columns alone.
|
||||
- **Left as they are:** the decoy salt never changes (an address's salt changes when it
|
||||
gets an account or a new password, which polling `begin` could see, as any SRP's
|
||||
does); SRP's x and the key-encryption key stay as plain bytes while a person types a
|
||||
code; the key proof's first step still gives anyone the account's public keys and its
|
||||
keys wrapped under the phrase (256 bits); an attacker failing from three networks or
|
||||
more can still hold an owner's sign-ins out for an hour at a time, and three requests
|
||||
void a code its owner is typing.
|
||||
|
||||
## Consequences
|
||||
|
||||
- People keep a password, an authenticator app and a recovery phrase: more to keep than a
|
||||
|
|
|
|||
Loading…
Reference in a new issue