feat(registry): TOTP second factor and recovery codes (#227) #272
Loading…
Reference in a new issue
No description provided.
Delete branch "loop/227-registry-totp"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #227. One commit; slice 5 of #16. Written without a local
torb, so CI is the first run.Per criterion
src/totp.trb, HMAC-SHA-1, 6 digits, 30 s steps, ±1 step. All window steps are compared in constant time. The last used step is stored and claimed byUPDATE ... WHERE last_step < stepwithchanges(), so a replay is refused. The clock is the database's, and a function argument in the pure part.POST /account/totp/new|add|remove|recovery. A 160-bit seed is shown once as anotpauth://URI and as Base32, with no script, no image and the CSP unchanged. A pending seed is kept 10 minutes. Enrolment needs a recent session and completes only on a valid code. A second enrolment is a 409. Removal needs a code or a recovery code and ends the other sessions.totp:<account>, nonce and ciphertext in one column.REGISTRY_SECRET_KEYis read inConfig. Unset or malformed, the registry logs that enrolment is off and the routes answer 503. A changed key makesopenfail, never accepts a code.DELETEwhosechanges()must be 1, all compared in constant time.recoveryregenerates them after a valid code. Deviation: they are stored as HMAC-SHA-256 under a key derived fromREGISTRY_SECRET_KEY, not as plain SHA-256. 50 bits of plain SHA-256 can be brute-forced from a database copy (found in architect review).code_attempts. A try is reserved in one atomic statement before the code is checked, so concurrent attempts cannot slip under the limit. A right code gives back only its own try, so a success does not reset an exceeded limit. Past the limit the answer is 429 withRetry-After, without checking the code.tests/totp.test.trbhas the RFC 4226 appendix D and RFC 6238 appendix B SHA-1 vectors, the window, sealing and recovery codes.tests/totp-account.test.trbruns the HTTP flow, including wrong, replayed and used-recovery codes, the limit, no key, a second enrolment and a stale session, and checks that no code or seed is stored in the clear or in events.REGISTRY_SECRET_KEY, design bullet, tests), RELEASE.md "Not built yet", pdisc restamped.Notes
waysToSignInand the "last way to sign in" rule are unchanged. The second sign-in step is #228.Registry.acceptedCode(account, code)is what #228 will call.add-totp,remove-totp,new-recovery-codesanduse-recovery-codewith an empty detail.36da6c7d9a173c9c66e6Approved at
173c9c66(CI success on this head, pdisc green, no tests skipped that matter).Checklist
UPDATE ... WHERE last_step < steppluschanges(); recovery codes byDELETEwithchanges()= 1; the attempt slot is reserved in oneINSERT ... SELECT ... WHERE count < limitbefore the code is looked at, so concurrent tries cannot pass the limit; a right code returns only its own try. The window loop inmatchedStepcompares every step in constant time. Recovery character draw is unbiased (byte & 31).totp:<account>as AAD; unset/malformed key means 503 and no unsealed storage; secrets are shown only onno-storepages with the CSP unchanged (no script/image); events carry empty detail; the recovery key is derived, not the sealing key itself.CREATE TABLE IF NOT EXISTSonly.REGISTRY_SECRET_KEY, design bullet, tests), RELEASE.md, sidecar hash restamped.Non-blocking
removeTotpandregenerateRecoveryCodesconsume the proof code (TOTP step or recovery code) before the follow-up transaction; if that transaction fails, the code is spent. Rare, and the user can retry with the next code.Merging by rebase.