Skip to content

docs: decide device approval is a bound rendezvous - #65

Merged
FSM1 merged 2 commits into
mainfrom
docs/adr-0009-device-approval
Aug 11, 2026
Merged

docs: decide device approval is a bound rendezvous#65
FSM1 merged 2 commits into
mainfrom
docs/adr-0009-device-approval

Conversation

@FSM1

@FSM1 FSM1 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Why this exists

v2 has no MFA and no device approval. The entire surface is one refusal in apps/web/src/auth/coreKit.ts: on REQUIRED_SHARE the 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:

  • The server can read the factor key. The API stores the requester's ephemeralPublicKey as 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.
  • Nothing is signed. deviceId and respondedByDeviceId are 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.
  • The human authorises on evidence they do not have — an attacker-supplied device name that passes validation as Chrome on macOS. The ephemeral key, the only value identifying what is being authorised, is shown to nobody.
  • The approver clones its own live factor key, and deleteFactor does 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

  • D1 — device approval is an API slice, and the blueprint stops calling it Core Kit UX.
  • D2 — the recovery phrase is the guaranteed path on every platform; device approval is additive. If D3/D4 cannot be met, approval does not ship and recovery carries the release.
  • D3 — both devices display a short comparison value derived from the ephemeral key, and the approver confirms the match before sealing. A substituted key becomes visible to the person authorising it.
  • D4 — both halves of the exchange are signed by device identity keys. The device key stops being inert.
  • D5 — an approval mints a fresh factor; the approver's own is never transferred. Removing a factor is described as what it is, not as retroactive revocation.

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.

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.
@FSM1

FSM1 commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Marked Accepted — not implemented in bb77797, matching how ADRs 0007 and 0008 record the same state. The status now names FSM1/cipher-box#1262 as the implementation umbrella.

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.

@FSM1
FSM1 merged commit 4248cf0 into main Aug 11, 2026
@FSM1
FSM1 deleted the docs/adr-0009-device-approval branch August 11, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant