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
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:
mainpost-v0.8.1 (2fa4d68tag; HEAD59ebc28) - Driver: Product audit
docs/superpowers/reviews/2026-06-20-product-audit.mdrecommendation #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.mdnotes "unlock takes JPEG bytes … extracts internally"). Add an explicitunlock_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>andkeyfile_decode(bytes) -> [u8;32]. Layout: arelicario-keyfile-v1header line + base64 of the 32 bytes + trailing newline.keyfile_decodevalidates the header, rejects malformed input, and holds the secret inZeroizing. Suggested extension:.relkey.
Stream decomposition
B1 · core + WASM
relicario-core:keyfile_encode/keyfile_decode(Zeroizing),unlock_with_secret, and read/write of thesecond_factorfield in the params struct (defaultimage).relicario-wasm: bindkeyfile_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.relkeyviakeyfile_encode, and sets paramssecond_factor: "keyfile"; the existing--image/--outputpath stays the default and sets"image".unlockacross all commands: read the factor per the params hint — ifkeyfile, from--key-fileorRELICARIO_KEYFILE(mirroringRELICARIO_IMAGE);keyfile_decode→unlock_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
.relkeyfor download, set the params hint. - Unlock: per the params hint, prompt for the key file (file picker) instead of the image;
keyfile_decode→unlock_with_secret. - Local storage (chosen default): store the key-file bytes in
chrome.storage.localaskeyfileBase64, re-read each unlock — exactly asimageBase64works 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 (
.relkeyarmor + paramssecond_factorfield).
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:
.relkeyandkeyfileBase64are the second factor in the clear — exactly the same posture as the reference JPEG /imageBase64today. Document this indocs/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_KEYFILEguidance. - Params
second_factorpresent but unknown value → reject with a clear message (forward-compat guard).
Testing
- core: keyfile encode/decode round-trip;
unlock_with_secretmaster-key equivalence vs JPEG unlock; params back-compat (absent ⇒ image); malformed-armor rejection. - CLI:
init --key-file → unlock --key-filelifecycle against a temp vault;RELICARIO_KEYFILEenv path; image vault still unlocks unchanged. - extension (vitest): setup key-file path writes the hint + offers download; unlock reads
keyfileBase64and 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.