ADR 0006: Existing-device pairing
Bind an expiring out-of-band pairing offer to a new browser key before an existing browser encrypts the root key for it.
Status: accepted for v0.3.
A new browser generates a non-extractable X25519 key and a random pairing request ID locally. It
publishes a versioned ownfold.pairing-offer containing the request ID, vault and device IDs,
device label, public key, creation time, and expiry. The default lifetime is ten minutes; servers
reject offers longer than fifteen minutes.
The user transfers the complete offer to an existing unlocked browser. That browser parses the offer, loads the authenticated user’s pending request, and compares every field exactly. It only then encrypts the root key to the public key authenticated by the out-of-band offer. This exact comparison prevents an application server from silently substituting its own recipient key.
Approval atomically changes the request from pending to approved and registers the device.
Drizzle and Prisma use transactions; the direct PostgreSQL adapter uses one data-modifying CTE.
Optimistic revisions prevent replayed approval and cancellation writes. The new browser polls the
request, authenticates and opens the device envelope locally, persists its non-extractable private
key in IndexedDB, and removes the temporary pairing state.
The application server sees pairing timing, labels, identifiers, public keys, status, and envelope size. It never receives a private key, root key, Recovery Kit, recovery password, or plaintext record. Expiry limits stale requests but does not defend a user who transfers an offer through a compromised channel or approves an offer they did not initiate.
Text transfer is the first UI. QR rendering may encode the identical versioned offer later; it must not introduce a second protocol or omit authenticated fields.