Files
relicario/docs/superpowers/specs/2026-06-20-pluggable-second-factor-design.md
adlee-was-taken 9b38aac188 docs(specs): v0.9.0 design — extension org GUI + pluggable second factor
Product audit (product-expert skill) recommended two priority items; this
lands the audit record plus the two approved design specs that will drive
the v0.9.0 multi-agent train.

- reviews/2026-06-20-product-audit.md — the roadmap audit (reality check,
  recommendations, PM brief) that drove the two items.
- specs/2026-06-20-extension-org-gui-design.md — bring the org vault to the
  extension at read+write parity. Org write is gated on a Day-1 signing
  spike (the org hook rejects unsigned commits; the extension pushes
  unsigned today; sign_for_git exists in WASM but is unused). Spike-fail
  degrades to read-only + write follow-up.
- specs/2026-06-20-pluggable-second-factor-design.md — key file as an
  alternative second factor (same 32-byte secret, same KDF; crypto-light),
  chosen at setup via a non-secret params hint, plus the positioning pivot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQbgrP6KQW5pibjbPEoTSs
2026-06-20 23:01:53 -04:00

7.4 KiB

Pluggable Second Factor (Key File) + Positioning Pivot — Design Spec

  • Date: 2026-06-20
  • Status: Approved (brainstorming) — ready for writing-plans
  • Release target: v0.9.0 (one multi-agent train, alongside the Extension Org Vault GUI spec)
  • Anchor: main post-v0.8.1 (2fa4d68 tag; HEAD 59ebc28)
  • Driver: Product audit docs/superpowers/reviews/2026-06-20-product-audit.md recommendation #2 (PIVOT) — re-lead positioning with the durable thesis (two secrets into the KDF + zero server metadata + git audit) and treat the steganographic image as one option for the second factor, with a plain key file as the alternative.
  • Builds on: 2026-04-11-relicario-design.md (crypto pipeline), docs/CRYPTO.md, docs/FORMATS.md, extension/ARCHITECTURE.md (setup wizard).

Purpose & scope

Make the vault's second factor pluggable: the 256-bit secret can be carried by the existing steganographic reference image (default) or by a plain key file — chosen at vault creation, with the same secret and the same KDF underneath. Re-lead the project's positioning on the durable thesis and frame stego as an option rather than the headline.

Key insight — this is crypto-light. The second factor is already just 32 bytes (image_secret); stego is only the storage/transport. The Argon2id KDF (passphrase || image_secret → master_key) and everything downstream are byte-for-byte unchanged. Only the source of the 32 bytes changes. No new crypto primitive.

Mental model (chosen in brainstorming): at creation you pick the container — Reference Image (default) or Key File — and both materialize the same random 32-byte secret. The vault records a non-secret container-type hint so unlock prompts for the right thing. Because it is literally the same secret, the recovery-QR already fits this model, and export/convert between containers is a natural (optional) add-on.

In scope: key-file generation at init; unlock from a key file; the non-secret container hint; CLI + extension support; the positioning/docs pivot.

Out of scope (optional stretch, not core): keyfile export / convert-an-existing-image-vault-to-keyfile. It needs the secret in hand (a re-provide-then-write flow), so it is deferred to keep the lift tight; noted as a fast-follow.

Crypto model

  • Container hint. .relicario/params.json (non-secret, already holds Argon2id params) gains "second_factor": "image" | "keyfile". Absent ⇒ "image" (back-compat for every existing vault). Read pre-unlock to choose the prompt; reveals nothing secret (container type is not a secret).
  • Raw-secret unlock path. Today unlock(passphrase, jpeg_bytes, salt, params) extracts the 32-byte secret from the JPEG internally (extension/ARCHITECTURE.md notes "unlock takes JPEG bytes … extracts internally"). Add an explicit unlock_with_secret(passphrase, secret: &[u8;32], salt, params) that skips extraction. The KDF and AEAD are identical; this is the only core seam.
  • Key-file armor. Core owns the format so CLI and WASM share it: keyfile_encode(secret) -> Vec<u8> and keyfile_decode(bytes) -> [u8;32]. Layout: a relicario-keyfile-v1 header line + base64 of the 32 bytes + trailing newline. keyfile_decode validates the header, rejects malformed input, and holds the secret in Zeroizing. Suggested extension: .relkey.

Stream decomposition

B1 · core + WASM

  • relicario-core: keyfile_encode / keyfile_decode (Zeroizing), unlock_with_secret, and read/write of the second_factor field in the params struct (default image).
  • relicario-wasm: bind keyfile_encode, keyfile_decode, unlock_with_secret.
  • Equivalence test: for a given 32-byte secret, unlock_with_secret(pass, secret, …) derives the same master key as unlocking from a JPEG that embeds that secret — proves the seam is transport-only.

B2 · CLI

  • relicario init: a container choice — --key-file <path> (or interactive) generates the 32-byte secret, writes the .relkey via keyfile_encode, and sets params second_factor: "keyfile"; the existing --image/--output path stays the default and sets "image".
  • unlock across all commands: read the factor per the params hint — if keyfile, from --key-file or RELICARIO_KEYFILE (mirroring RELICARIO_IMAGE); keyfile_decodeunlock_with_secret.
  • Help text + docs/user_docs/ reflect the choice.

B3 · extension

  • Setup wizard step 3 gains a container choice (Reference Image | Key File). Key-file mode: generate the secret, offer the .relkey for download, set the params hint.
  • Unlock: per the params hint, prompt for the key file (file picker) instead of the image; keyfile_decodeunlock_with_secret.
  • Local storage (chosen default): store the key-file bytes in chrome.storage.local as keyfileBase64, re-read each unlock — exactly as imageBase64 works today. Same "something you have" threat model and the same offline behavior; documented as equivalent, not weaker.

B4 · docs / positioning pivot

  • README re-led: open with the thesis (two independent secrets into the KDF, self-host, zero server metadata, git audit); present the steganographic image as a distinctive option for the second factor (with the key file as the plain alternative), not the headline. Keep the dead-drop story as flavor, not the lead.
  • DESIGN.md secrets-map + docs/CRYPTO.md (pluggable-transport framing: "the second factor is 32 bytes; image/key-file/recovery-QR are interchangeable containers") + docs/FORMATS.md (.relkey armor + params second_factor field).

Security-review gate (before merge)

A focused /security-review pass on the key-file path:

  • No weaker than stego: same 32-byte entropy, same Argon2id, same AEAD. The equivalence test (B1) is the evidence.
  • Armor parsing rejects malformed/short input without panics or oracles.
  • Threat-model honesty: .relkey and keyfileBase64 are the second factor in the clear — exactly the same posture as the reference JPEG / imageBase64 today. Document this in docs/SECURITY.md; do not imply the key file is encrypted (it is the "something you have", protected by needing the passphrase too).
  • No oracle differences between the image and key-file unlock failure paths (both surface the deliberately-ambiguous "wrong passphrase or reference image/key").

Error handling

  • Malformed/empty key file → invalid_key_file (CLI + extension), distinct from a wrong-secret AEAD failure.
  • Missing key file at unlock when params say keyfile → prompt/RELICARIO_KEYFILE guidance.
  • Params second_factor present but unknown value → reject with a clear message (forward-compat guard).

Testing

  • core: keyfile encode/decode round-trip; unlock_with_secret master-key equivalence vs JPEG unlock; params back-compat (absent ⇒ image); malformed-armor rejection.
  • CLI: init --key-file → unlock --key-file lifecycle against a temp vault; RELICARIO_KEYFILE env path; image vault still unlocks unchanged.
  • extension (vitest): setup key-file path writes the hint + offers download; unlock reads keyfileBase64 and derives a session; an existing image vault is unaffected.

Living-docs impact

README.md, DESIGN.md, docs/CRYPTO.md, docs/FORMATS.md, docs/SECURITY.md, crates/relicario-core/ARCHITECTURE.md (new core functions), extension/ARCHITECTURE.md (setup container choice + keyfileBase64), CHANGELOG.md.