carlos / contra Public

DECISION NEEDED: do we print a 128-bit Account Key beside the recovery code? Question closed

On branch intent

The one open decision in DESIGN.md. It needs Paul, because it changes a promise we have already written down.

The problem

With passphrase-only unlock, the passphrase wrap W_pw sits in the replicated bucket as an offline dictionary target, bounded only by ~1 second of Argon2id per guess. A human-typical passphrase falls to that. Put plainly: bucket security equals passphrase strength, and no amount of good design elsewhere changes it.

Option A — print an Account Key (the reviewer's recommendation)

The other half of 1Password's Emergency Kit: a 128-bit Account Key on the same sheet as the recovery code, mixed into the KEK branch (HKDF(S_pw, account_key)), cached per enrolled device, required once per new device.

  • Buys: the bucket becomes uncrackable regardless of how bad the passphrase is.
  • Costs: a genuinely fresh browser needs the sheet, or a QR handoff from an enrolled device — a stricter reading of "a passphrase signs you in to the web console".
  • New fatal loss mode: all devices lost, paper lost, passphrase remembered becomes unrecoverable. Our current promise says the passphrase or the codes gets you back. Option A makes that untrue.

Option B — no Account Key (what was originally specified)

The passphrase alone gets you in from anywhere. Then three compensating controls become mandatory rather than nice-to-have: a generated passphrase the UI pushes hard, an enforced zxcvbn floor, and a published threat model saying plainly that bucket security equals passphrase strength.

What is already done either way

DESIGN.md specifies the KEK branch with the account-key input present and set to a constant, so choosing A later is a re-wrap and not a redesign. Nothing is blocked on this decision — but it should be settled before signup ships, because it changes the print sheet and the copy around it.

The question in one line

Is "you can always get back in with just your passphrase" a promise we want to keep, knowing it is also the ceiling on how strong the vault can be?

Decided (Paul, 2026-09-01): Option A. The Account Key is printed on the kit alongside the recovery code, and the fresh-device cost is handled with UX rather than by giving up the protection.

Settled in DESIGN.md — "The Emergency Kit". What changed:

  • KEK_pw = HKDF-SHA-256(S_pw, salt=account_key, info="contra/v1/kek"). The input was already specified present-and-constant, so this was the re-wrap it was designed to be.
  • W_rc and W_prf stay peer wraps, not gated on the Account Key. Gating recovery on it too would add nothing — both values live on one sheet, so whatever compromises one compromises the other — while adding a second thing to go wrong on the worst day of a user's life.
  • The compromise matrix improves in the row that matters: a passive server-plus-bucket leak now has no offline dictionary target at all. Every wrap is keyed by at least 128 bits of full entropy.

The kit is a client-generated PDF that never touches the server, laid out in two separated panels with a fold line. That layout is doing security work, not decoration: the Account Key gets handled routinely — every new device wants it — while the recovery code should ideally never leave the paper. Printed adjacent, routine handling of one exposes the other to every phone camera in the room. One panel is designed to be photographed; the other is designed not to be.

The UX layer, cheapest rung first and never dead-ending:

  1. Handoff from an enrolled device — the new browser mints an ephemeral keypair, the server relays a blob it cannot open, an enrolled device confirms a short code. Slopbox's link pattern, reused rather than reinvented.
  2. Scan the QR on the kit.
  3. Type it — Crockford Base32, self-healing checksum, last resort.

Once cached, the passphrase alone unlocks that device, so the everyday case is exactly what was originally promised.

The cost, recorded honestly: "passphrase known, kit lost, every device gone" is now unrecoverable. That is new, and it is in the README without softening, next to the reason we think it is worth it. Two things follow and are now tasks — a prominent reprint flow (the Account Key reprints exactly, since enrolled devices cache it; the recovery code cannot be reprinted, only regenerated, and the copy says so before the user commits), and nagging people to enrol a second device.

Closing this.

Two refinements from Paul, 2026-09-01, after the decision above.

1. The kit is two PDFs, not one sheet with a fold line. The earlier draft was too weak, and the reason is that the two values have different blast radii — they are not two halves of one secret:

  • The Account Key alone is nearly harmless. One input to the passphrase branch, useless without the passphrase.
  • The recovery code alone is full takeover, through the online recovery flow.

One is a setup aid, the other a bearer credential for the whole vault. And the risk we are actually managing is storage, not printing: people save PDFs, and one file puts both secrets in the same folder forever. A fold line does nothing about that; two files can go to two places.

So: an Account Key card (wallet-sized, keep it handy, photograph it without guilt) and a Recovery sheet (archival, never photographed, safe or deposit box). Signup generates both, offers one "print both" action, and does not let the user past until both are confirmed.

A consequence worth recording: this invalidated the reason I gave for not gating recovery on the Account Key. I had written "both values live on the same sheet, so anything compromising one compromises the other" — which stops being true the moment they are separate documents.

The decision does not change, but the real reason is better. Gating recovery on the Account Key is rejected because of which document survives what. The card is the handy one, living beside your devices because that is where it gets used. The sheet is the archived one, kept elsewhere precisely so it outlives a disaster at home. A fire or a stolen bag that takes your devices very likely takes the card next to them — so gating recovery on the card would make the archived sheet useless in exactly the scenario it exists for. The sheet must be sufficient alone; what protects it is its handling plus the notify-and-delay policy on the recovery flow.

2. Unrecoverability is the point, not a cost. Paul's correction, and the docs were wrong. I had written the new loss mode up as a price paid for the Account Key. It is not: a vault the operator cannot open is a vault nobody can reopen for you, and applying that without exception is the rule working.

What genuinely changes is a feeling, not a rule — "I forgot my passphrase" and "I know my passphrase and lost the paper" land very differently on a person, though the principle is identical. So the obligation is not to soften the rule but to make sure nobody arrives there by accident: the reprint flow and the second-device nag stay, and the copy states the rule plainly without apologising for it.

This also retires a promise that was never quite true once the Account Key existed — "your passphrase or your codes get you back in." The honest model is simpler: the kit is the root; the passphrase is the everyday convenience.