docs: re-lead positioning on the two-factor-KDF thesis; document the key-file second factor

README: open with the two-independent-secrets-into-KDF thesis; reframe the
second factor as pluggable (reference image or key file); keep the stego
deep-dive as a distinctive option; update all scenario tables, recovery section,
env-var hint, and roadmap line to use "second-factor secret" language.

docs/CRYPTO.md: add "Pluggable second-factor containers" subsection explaining
that the 32-byte input to Argon2id is identical regardless of container;
reference-image / key-file / recovery-QR are interchangeable carriers.
Update org-vs-personal comparison table and entropy labels.

docs/FORMATS.md: document the .relkey armor format (relicario-keyfile-v1 +
base64 line) and the params.json second_factor field ("image"|"keyfile",
absent means "image" for back-compat). Cite crates/relicario-core/src/keyfile.rs
keyfile_encode/keyfile_decode with TODO-pin comments. Update KDF input formula
from image_secret to second_factor_secret.

DESIGN.md: split secrets-map "Reference image bytes" row to distinguish image
and key-file containers; add key-file row noting keyfileBase64 in
chrome.storage.local has the same in-the-clear posture as imageBase64.
Update vault-layout note and KDF conventions row.

docs/SECURITY.md: add "Second-factor storage posture" subsection explicitly
stating .relkey and keyfileBase64 are the second factor in the clear — same
posture as the reference JPEG / imageBase64, not weaker, protected by the
passphrase invariant. Add RELICARIO_KEYFILE to the env-var trust surface table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
This commit is contained in:
adlee-was-taken
2026-06-25 21:24:06 -04:00
parent a25f8e8a30
commit 1591fc7825
5 changed files with 100 additions and 39 deletions

View File

@@ -63,6 +63,20 @@
└──────────────────────────────────────────────────────────────────┘
```
## Pluggable second-factor containers
The second factor input to Argon2id is always **32 bytes** (the `image_secret`
field in the KDF call). The *container* that holds those 32 bytes is chosen at
vault creation and is interchangeable:
| Container | How the 32 bytes are stored | Extract step |
|---|---|---|
| Reference image (default) | Embedded in JPEG DCT coefficients via QIM steganography | `imgsecret::extract()` |
| Key file (`.relkey`) | base64-encoded in a plain armored text file | `keyfile_decode()` (`crates/relicario-core/src/keyfile.rs` — <!-- TODO(dev-e): pin exact line once Dev-D's keyfile.rs lands -->) |
| Recovery QR | XChaCha20-Poly1305 envelope keyed by a separate recovery passphrase | Unwrap then read the raw 32 bytes |
The Argon2id KDF input — `u64_be(len(passphrase)) || passphrase || u64_be(32) || secret_32_bytes` — is **identical regardless of container**. Master-key derivation is unaffected by the container choice. The diagrams below show the reference-image path; substitute the appropriate extract step for other containers.
## Vault Creation Flow
```
@@ -228,7 +242,7 @@ Unlike the personal vault, **org crypto bypasses Argon2id entirely**:
| | Personal vault | Org vault |
|---|---|---|
| Key origin | Argon2id(passphrase ‖ image_secret, salt) | OsRng → 256-bit random |
| Key transport | Embedded in reference JPEG (stego) | X25519 ECIES wrap blob |
| Key transport | Embedded in reference JPEG (stego) or stored in a `.relkey` key file | X25519 ECIES wrap blob |
| AEAD primitive | XChaCha20-Poly1305 (`crate::crypto::encrypt`) | Same primitive (delegated) |
| KDF for wrap key | Argon2id | SHA-256(DH ‖ eph_pk ‖ rec_pk) |
@@ -414,13 +428,13 @@ Input JPEG (possibly re-encoded or cropped)
```
Server breach only: ████████████████████████████ 256+ bits (infeasible)
passphrase + image_secret
passphrase + second-factor secret
Server + stolen image: ████░░░░░░░░░░░░░░░░░░░░░░ ~51 bits (4 diceware words)
Server + stolen 2nd factor: ████░░░░░░░░░░░░░░░░░░░░░░ ~51 bits (4 diceware words)
passphrase through Argon2id ~7 million years
Shoulder-surfed passphrase: ████████████████████████████ 256 bits (infeasible)
image_secret
second-factor secret
Stolen device: ████░░░░░░░░░░░░░░░░░░░░░░ ~51 bits (4 diceware words)
passphrase through Argon2id ~7 million years