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 20260412
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

  1. Make the Secret Key hard to lose (physical print, multiple device

    copies, exportable backup files with clear warnings).

  2. Make device enrollment fast on the happy path (<30 seconds) and

    possible without Secret Key when at least one already-authorized device is online.

  3. Make Secret Key rotation cryptographically cheap and atomic — no

    item re-encryption required.

  4. Leave no ambiguity about irreversible operations: rotations and

    password changes show explicit lock/unlock copy, not "save" buttons.

  5. 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*(Applestyle, Signalstyle) —

    possible in a future RFC, but adds cryptographic + UX complexity we don't need for v1.

  • *ardwaretokenonly 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 the

    KDF 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:

  1. *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."

  2. *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.

  3. *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

  1. *itonDrive trade-off*— storing the Emergency Kit PDF in Koder

    Drive 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".

  2. *haredteam vaults and the kit model*— orgowned vaults need

    delegation that an individual Secret Key cannot express. Defer to RFC-014c once team vaults are in scope.

  3. *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.

  4. *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.

Source: ../home/koder/dev/koder/meta/docs/stack/rfcs/id-RFC-014a-emergency-kit-recovery.md