docs: decide device approval is a bound rendezvous - #65
Conversation
v2 has no MFA and no device approval — the whole surface is a REQUIRED_SHARE refusal in the web client. The blueprint calls the missing slice chrome-side Core Kit UX in three files, which is why nothing tracks it: an existing device approving a new one needs a server-mediated rendezvous, and v1 built an API module with its own table to provide one. v1's shape was sound and four of its properties were not. The server can substitute the relayed ephemeral key and receive the factor key sealed to itself; under ADR 0008 that same operator mints the identity token, so both halves of the threshold sit with one party. Neither half of the exchange is signed. The approver authorises on an attacker-supplied device name. And the approver clones its own live factor key. Decides the recovery phrase as the guaranteed path, an out-of-band comparison of the ephemeral key, signed binding by device identity keys, and a fresh factor per approval.
|
Marked Accepted — not implemented in That umbrella carries six sub-issues with dependency edges: FSM1/cipher-box#1264 (the API rendezvous), FSM1/cipher-box#1265 (the recovery-phrase login path), FSM1/cipher-box#1266 (a device identity key that signs), FSM1/cipher-box#1267 (the UI and the out-of-band comparison), FSM1/cipher-box#1268 (desktop's recovery path and approval role) and FSM1/cipher-box#1269 (a two-session test harness). FSM1/cipher-box#1265 is the one to start on — the recovery path is the floor this decision rests on and it depends on nothing else here. The blueprint statement is FSM1/cipher-box#1263, held as a draft and stacked on FSM1/cipher-box#1255 because both edit the same login sections. |
Why this exists
v2 has no MFA and no device approval. The entire surface is one refusal in
apps/web/src/auth/coreKit.ts: onREQUIRED_SHAREthe client logs out, clears its store and throws, with a comment saying recovery and device approval "are not built yet". No API endpoint, no engine surface, no UI — and no issue tracking any of it.The blueprint is why it went unnoticed. Three files (
web-client.md,desktop.md,testing.md) call MFA and device approval "chrome-side" Core Kit UX. It cannot be: an existing device approving a new one needs a server-mediated rendezvous, and v1 built an API module with its own table and migration to provide exactly that. The mislabel concealed an API slice.What the v1 evidence changed
The shape was right — the API is a bulletin board relaying ciphertext, and the scoped pre-reconstruction token is properly limited and fails closed. Four properties were not, and they are why this is a decision rather than a port:
ephemeralPublicKeyas opaque text and echoes it to the approver, who seals to whatever the response contains. Substitute it and the factor key arrives sealed to the server. Under ADR 0008 the same operator mints the identity token that yields the verifier share — factor key plus verifier share is the threshold, so both halves sit with one party.deviceIdandrespondedByDeviceIdare self-reported strings, so the self-approval check compares two attacker-controlled values. v1 minted a per-device Ed25519 key, wrapped it under HKDF of the vault key, and never signed anything with it.Chrome on macOS. The ephemeral key, the only value identifying what is being authorised, is shown to nobody.deleteFactordoes not rotate anything.The gap was decided into existence, not overlooked: v1's research asked whether a confirmation code was needed and concluded ECIES was sufficient because "the ephemeral key exchange ensures only the requesting device can decrypt" — reasoning that treats a server-relayed public key as trustworthy. "MITM" appears nowhere in the v1 MFA corpus.
One scope-relevant fact: v1's cross-device cases were all skipped for want of two authenticated devices, and the desktop half sent a 33-byte compressed key where the DTO demanded 65 uncompressed — a guaranteed rejection on every attempt, marked verified on grep-shaped evidence.
The decisions
The product question inside D2
Recovery-phrase-only is a real option and is argued in the rejected alternatives: it deletes the rendezvous, the table, the polling and the attack surface, and v1's cross-device path was never exercised. It is rejected as the whole answer because a phrase is a single artefact a member can lose and lockout is unrecoverable here — but it is adopted as the floor. If you would rather 2.0 ship recovery-only and defer approval entirely, D2 is the clause to change and the rest of the ADR still stands.
Implemented by FSM1/cipher-box#1262. Blueprint statement in FSM1/cipher-box#1263.