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
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# 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_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 `.relkey` for 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.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`.
|
||||
Reference in New Issue
Block a user