Id RFC 014a emergency kit recovery
RFC-014a — Emergency Kit Recovery UX for Koder Keys
| Field | Value |
|---|---|
| Status | *raft* |
| Author(s) | Rodrigo (with Claude as scribe) |
| Date | 2026 |
| Depends on | RFC-014 (Vault / Koder Keys) |
| Affects | services/keys/, CLI kode keys, Koder Keys mobile client, Koder ID account settings |
1. Summary
RFC014 established that Koder Keys is zeroknowledge by construction: the server holds only ciphertext, and the Vault Data Key (VDK) is derived clientside from the account password plus a 34character *ecret Key*printed in the emergency kit. This sub-RFC specifies the concrete UX and cryptographic rituals for every moment the Secret Key enters or leaves the system: account creation, device enrollment, password change, Secret Key rotation, and recovery from a lost device.
The governing principle is stated once, then repeated everywhere it matters: *oder never has, and never can have, a way to unlock a Vault whose owner has lost both the password and the Secret Key.*The entire recovery story is about making it rare for users to arrive at that cliff, not about rescuing users who have.
2. Goals
- Make the Secret Key hard to lose (physical print, multiple device
copies, exportable backup files with clear warnings).
- Make device enrollment fast on the happy path (<30 seconds) and
possible without Secret Key when at least one already-authorized device is online.
- Make Secret Key rotation cryptographically cheap and atomic — no
item re-encryption required.
- Leave no ambiguity about irreversible operations: rotations and
password changes show explicit lock/unlock copy, not "save" buttons.
- Preserve zero-knowledge: no step in any flow transmits password,
Secret Key, MUK, or VDK to the server.
3. Non-goals
- *oder-held escrow*— explicitly out; would break the trust model.
- *ocial recovery / trusted contacts*(Apple
style, Signalstyle) —possible in a future RFC, but adds cryptographic + UX complexity we don't need for v1.
- *ardware
tokenonly flows (FIDO2 as Secret Key replacement)*—covered by a separate RFC-014b when WebAuthn device attestation is reliable across the client fleet.
- *elf-service "I forgot my password but still have the Secret Key"
reset flow for business tenants*— requires policy hooks from the admin service, punt.
4. The Secret Key — anatomy
4.1 Format
A3-BHKNM-Q2X8R-YDVJ6-FT4WS-P9CZE-7GL- 34 characters total, 7 dash-separated groups
- Base32 alphabet (Crockford variant: no 0O1IL ambiguity)
- Group 1 is a 2-character *ccount version prefix*(
A3= v3 of theKDF params; clients use it to pick the right Argon2id parameters)
- Groups 2–7 carry 30 base32 characters of entropy = 150 bits
- A Luhn-style checksum is folded into the last group so pasted Secret
Keys can fail fast on typos without needing a server round-trip
4.2 Lifecycle
The Secret Key is generated exactly once, on the client, at the moment the user completes the Koder Keys onboarding wizard. It is:
- Shown on screen with a "Print now" and a "Save to Koder Drive"
button side by side
- Rendered into a printable *mergency Kit PDF*that includes:
- The Secret Key itself (large, with groups spaced clearly)
- A QR code containing the Secret Key (for easy device enrollment)
- The account email
- A "Not a password. This is a single-use backup." warning
- Room for the user to write their password (specifically: room they
can leave blank — the label literally says "(optional, for you to write your password in ink if you know what you're doing)")
- Stored on-device under the platform secure enclave (Android
Keystore / macOS Keychain / Linux secret-service) so authenticated sessions can derive the VDK without re-prompting
- Never transmitted to the Koder server in plaintext form
- Never stored in cleartext on disk outside the enclave
4.3 Server-side
The server stores only secret_key_hash in the vaults row (Argon2id of the Secret Key salted with the account id). This exists solely so devices can verify on enrollment that the user typed the correct Secret Key before triggering an expensive KDF — not for server-side authentication of vault unlock. A matching hash grants nothing beyond "OK to proceed with local key derivation"; an attacker who steals the hash still needs the password to derive MUK and then VDK.
5. Flow 1 — Account creation (new vault)
Step 1 User on a signed-in Koder ID device opens Koder Keys for the first time.
Step 2 Client shows "Create your Vault" with 3 sub-steps visible as progress.
Step 3 Client generates Secret Key locally (150 bits of CSPRNG entropy, CheckedAlphabet).
Step 4 Client prompts for a vault password (can be same or different from Koder ID password;
default UX is "use your Koder ID password" with a disclosure toggle).
Step 5 Client runs Argon2id(password, salt=Secret Key ‖ account_id) → MUK → VDK.
Step 6 Client computes secret_key_hash = Argon2id(Secret Key, salt=account_id).
Step 7 Client POSTs /v1/keys/bootstrap { secret_key_hash, vdk_fingerprint, kdf_params_version }.
Step 8 Server creates the vaults row, returns 201 Created.
Step 9 Client writes the Secret Key + VDK to the platform secure enclave.
Step 10 Client generates the Emergency Kit PDF and presents the "print / save / continue" gate:
- "Print now" (opens system print dialog)
- "Save to Koder Drive" (uploads the PDF to the user's Drive, in a folder marked
"Sensitive — print me and delete from Drive")
- "I have saved it" (checkbox, disabled for 10 seconds to discourage blind clicks)
Step 11 Client transitions to the empty vault. Onboarding is done.The 10-second delay on "I have saved it" is deliberate — users who click through instantly are the ones who lose their Secret Key. The cost of 10 seconds of annoyance is rounding error against the cost of a permanently inaccessible vault.
6. Flow 2 — Enrolling an additional device (happy path)
Two devices are needed for this flow: the new device ("B") and any existing authorized device ("A"). Both must be online.
On device B: Open Koder Keys → "Enroll this device".
Client prompts: "Open Koder Keys on a device you've already
set up. Scan the QR code you'll see."
Client generates an ephemeral X25519 keypair (b_priv, b_pub).
Client displays b_pub as a QR.
On device A: Koder Keys receives a notification via the Koder ID session
service: "A new device wants to enroll."
User taps → device A opens the camera to scan device B's QR.
After scanning, device A:
- derives a shared secret via X25519(a_secret, b_pub)
where a_secret is an ephemeral key derived from VDK + timestamp
- encrypts the Secret Key + current VDK with the shared secret
(AES-256-GCM)
- POSTs /v1/keys/enroll { to_device_id, ciphertext, nonce }
On server: Server holds the ciphertext for up to 5 minutes, then expires it.
Does NOT log, does NOT replicate across tenants.
On device B: Client polls /v1/keys/enroll/pending, receives the ciphertext,
decrypts with its b_priv + the shared secret,
extracts the Secret Key + VDK,
stores in the secure enclave.
Enrollment complete. Vault is unlocked.No Secret Key is ever typed during this flow. No master password leaves device A. The server sees only an opaque ciphertext. Five-minute expiry is a backstop against interception via pending-enrollment harvesting.
7. Flow 3 — Enrolling without another device (cold start)
Used when: user wipes all devices, or onboarded a Vault on device A, printed the kit, then lost device A.
Step 1 On the new device, user opens Koder Keys → "Enroll with Emergency Kit".
Step 2 Two fields: account password + Secret Key (with the option to scan the
kit's QR code instead of typing).
Step 3 Client rejects obviously-wrong Secret Keys via the checksum before
any network call.
Step 4 Client computes secret_key_hash locally and GETs
/v1/keys/bootstrap-challenge?secret_key_hash=... which returns either
"hash matches, proceed" or "hash mismatch".
- On mismatch: rate-limited retry (5/hour per account) and a copy
that reassures the user the server is not under attack, they just
typed something wrong.
Step 5 Client runs Argon2id(password, salt=Secret Key ‖ account_id) → VDK.
Step 6 Client attempts to decrypt one known item (or the vdk_fingerprint
probe blob) to verify the VDK is correct.
- If fingerprint fails: "Your password doesn't match this vault.
Try again." Does not leak whether the Secret Key or the password
was wrong — the combined failure looks the same.
Step 7 On success, client persists both to the enclave and the vault opens.This flow is intentionally slower and scarier than Flow 2. "Use your Emergency Kit" should feel like unsealing an envelope, not signing in.
8. Flow 4 — Lost Secret Key but still have a device online
Precondition: User is logged into Koder Keys on at least one device.
Step 1 Settings → "Security" → "Rotate Emergency Kit".
Step 2 Two-factor gate: re-enter the vault password + a fresh
Koder ID MFA challenge (so a passing stranger can't do this).
Step 3 Client generates a new Secret Key and a new Argon2id salt.
Step 4 Client re-derives MUK with the new salt (same password).
Note: VDK is NOT rotated. Item ciphertexts are unchanged.
Step 5 Client PUTs /v1/keys/rotate-secret-key { new_secret_key_hash,
new_kdf_params }.
Step 6 Server atomically replaces secret_key_hash + kdf_params.
Bumps the vault's version.
Step 7 All *other* devices belonging to this account receive a
"Re-enroll your Emergency Kit" notification — not a forced
re-enroll, but a clear status banner that the old kit is
no longer valid. Each device prompts the user to enter the
new Secret Key on its next unlock (pulled from the enclave
on the device that ran the rotation).
Step 8 New Emergency Kit PDF is generated and offered to print/save.Critically: because VDK does not change, no item re-encryption happens. The rotation is O(1) server-side and instantaneous from the user's perspective. This is only possible because our KDF chain is password → (with salt = Secret Key) → MUK → VDK — rotating the salt reaches only the top of the chain.
9. Flow 5 — Changing the vault password
Step 1 Settings → "Security" → "Change vault password".
Step 2 Re-enter current password (verifies by deriving MUK and comparing
vdk_fingerprint).
Step 3 Enter new password twice.
Step 4 Client derives new MUK with new password + existing Secret Key.
Step 5 VDK is NOT rotated; items are untouched.
Step 6 No server call is strictly required — the server doesn't hold the
password. But we PUT /v1/keys/password-changed { bump_version_only }
so other devices receive a "re-unlock with your new password"
notification.10. Flow 6 — Rotating the VDK (post-compromise)
The nuclear option, used if the user believes their VDK may have leaked (e.g., from a compromised device whose enclave was extracted).
Step 1 Settings → "Security" → "Re-encrypt all items (rotate VDK)".
Step 2 Strong confirmation dialog. Shows estimated time = item count × 50ms.
Step 3 Client generates a new VDK (random 256-bit).
Step 4 Client fetches all items (the ciphertexts), decrypts each with old
VDK, re-encrypts with new VDK, and PUTs them back one at a time
(not batched — per RFC-014 §4.3, "vaults are small; no batch API
needed").
Step 5 Client updates vdk_fingerprint on the vault row.
Step 6 Client logs out all other devices and forces them to re-enroll
via Flow 2 or Flow 3.This is the only flow in the RFC where the client does O(n) work. It is expected to take seconds, not minutes, for any realistic personal vault size. A progress bar is mandatory; interruption is tolerated via resumefromlastcompleteditem.
11. Failure modes the recovery UX must handle
| Scenario | Recovery path |
|---|---|
| Forgot password, still have Secret Key | ❌ Not supported in v1 (see §3 Non-goals) |
| Forgot Secret Key, still have online device | ✓ Flow 4 (rotate Secret Key) |
| Lost all devices, still have printed kit | ✓ Flow 3 (cold-start enrollment) |
| Lost kit *nd*password, devices lost | ❌ Vault permanently inaccessible |
| Kit falls into attacker hands, password still strong | ⚠ Attacker can attempt offline Argon2id; mitigate via password strength + rotate Secret Key ASAP via Flow 4 |
| Attacker steals device (hot, unlocked) | Revoke session via Koder ID; Flow 6 if VDK suspected leaked |
12. Copy principles
Every piece of user-facing text in these flows follows three rules:
- *ame the irreversible thing plainly*— "If you lose your
Emergency Kit *nd*your password, we cannot help you recover this vault. Nobody can. That is the point."
- *void security jargon in the primary path*— "Emergency Kit",
not "34-char recovery entropy". "Vault password", not "KDF input". Jargon is allowed in an "About this kit" collapsible for security-curious users.
- *o fake buttons that do nothing*— every click moves state.
"I have saved it" must actually persist the user's attestation so we can audit how many users click through blind (telemetry counts dwell time, not identities).
13. Open questions
- *it
onDrive trade-off*— storing the Emergency Kit PDF in KoderDrive is convenient but moves the kit into an online system, which is philosophically at odds with "print it and put it in your drawer". Recommendation: allow it, but the PDF on Drive is itself encrypted with a separate derived key, and the UI never calls it "backup" — only "copy of printable kit".
- *hared
team vaults and the kit model*— orgowned vaults needdelegation that an individual Secret Key cannot express. Defer to RFC-014c once team vaults are in scope.
- *IDO2 as Secret Key replacement*— a hardware-backed keypair
could replace the 34-char string entirely on supported platforms. The ergonomics are compelling; the cross-platform story is not yet. RFC-014b.
- *rinted QR code vs. typed string*— both, in v1. The typed
string is the source of truth for auditability; the QR is a convenience. Dropping the typed string would lock users to devices with working cameras.
14. Appendix — what Koder can help with, and what it can't
| Koder can… | Koder cannot… |
|---|---|
| Confirm that a Secret Key hash matches the server's | Recover a lost Secret Key |
| Rate-limit unlock attempts | Decrypt any vault item |
| Revoke device sessions | Reset the vault password server-side |
| Preserve 90 days of item version history | Recover items older than the window |
| Show users when their password is reused elsewhere | Access the password itself |
This table appears verbatim in the Koder Keys onboarding wizard and in the About → Security page of the app, so users see the honest version of the trust model before they commit a single secret.