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

@@ -6,7 +6,9 @@
> **Audience:** users + evaluators. This doc owns the pitch, security-model summary, quick-start commands, reference-image explanation, recovery-QR overview, and roadmap teaser. Goes no deeper — for the system tour see [DESIGN.md](DESIGN.md), for crypto see [docs/CRYPTO.md](docs/CRYPTO.md).
A git-backed, self-hostable password manager where decryption requires two independent factors: a passphrase you memorize and a reference JPEG that carries a hidden secret. Compromise of either factor alone is insufficient.
A git-backed, self-hostable password manager. The security thesis: two independent secrets feed the Argon2id KDF, the vault server holds only opaque ciphertext, and every mutation is a git commit — a built-in audit log. Compromise of either factor alone is insufficient to decrypt the vault.
The **second factor** (something you have) is pluggable: at vault creation you choose a **reference image** (a JPEG that carries a 32-byte secret hidden in its DCT coefficients via steganography) or a **key file** (the same 32-byte secret in a plain armored text file). Either container enters the KDF identically; the choice is how you store and transport it.
The server only ever sees opaque ciphertext. There is nothing else going on. This README is the security proof.
@@ -15,7 +17,7 @@ The server only ever sees opaque ciphertext. There is nothing else going on. Thi
```
Your passphrase (something you know)
+
Your reference photo (something you have)
Your second factor — reference image or key file (something you have)
|
v
[ Argon2id KDF ] --> master_key --> [ XChaCha20-Poly1305 ] --> encrypted vault
@@ -25,9 +27,9 @@ Your reference photo (something you have)
your device (opaque ciphertext)
```
At vault creation, Relicario embeds a random 256-bit secret into a carrier JPEG using DCT steganography. This photo becomes your **reference image** — a second factor that lives on your devices (and optionally as a "dead drop" on social media, since it survives JPEG re-encoding and mild cropping).
At vault creation you choose a **second-factor container**. The default is a **reference image**: Relicario generates a random 32-byte secret and embeds it into a carrier JPEG using DCT steganography. The resulting photo lives on your devices and optionally as a "dead drop" on social media, since the embedding survives JPEG re-encoding and mild cropping. The alternative is a **key file**: the same 32-byte secret written to a plain armored `.relkey` file, suited to users who prefer copying a file to a USB drive rather than distributing a photo.
To unlock the vault, you provide your passphrase and point the client at the reference image. The client extracts the hidden secret, concatenates it with your passphrase, and runs Argon2id to derive the master key. Everything else follows from there.
To unlock the vault, you provide your passphrase and your second-factor container (reference image or key file). The client extracts the 32-byte secret, concatenates it with your passphrase, and runs Argon2id to derive the master key. The KDF input is byte-for-byte identical regardless of which container you chose. Everything else follows from there.
## Security model
@@ -42,16 +44,16 @@ A git repository containing:
- `.relicario/params.json` — Argon2id parameters (not secret)
- `.relicario/devices.json` — authorized device public keys
That's it. No plaintext. No metadata about what's inside. No keys, no passphrases, no reference images.
That's it. No plaintext. No metadata about what's inside. No keys, no passphrases, no second-factor containers (no reference images, no key files).
### What an attacker needs
| Scenario | Has | Needs | Result |
|---|---|---|---|
| Server breach | Encrypted vault + salt | Passphrase AND image secret | 256+ bits of entropy. Infeasible. |
| Server breach + stolen image | Vault + image secret | Passphrase | Passphrase entropy through Argon2id. 4 diceware words = ~7 million years. |
| Shoulder-surfed passphrase | Passphrase | Image secret | 256 bits. Infeasible. |
| Stolen device | Image + vault | Passphrase | Argon2id brute-force. Strong passphrase = safe. |
| Server breach | Encrypted vault + salt | Passphrase AND second-factor secret | 256+ bits of entropy. Infeasible. |
| Server breach + stolen second factor | Vault + second-factor secret | Passphrase | Passphrase entropy through Argon2id. 4 diceware words = ~7 million years. |
| Shoulder-surfed passphrase | Passphrase | Second-factor secret | 256 bits. Infeasible. |
| Stolen device (image or key file + vault) | Second factor + vault | Passphrase | Argon2id brute-force. Strong passphrase = safe. |
No single point of failure. The two-factor design means the passphrase alone can't decrypt the vault, and the image alone can't decrypt the vault.
@@ -62,12 +64,12 @@ No single point of failure. The two-factor design means the passphrase alone can
| LastPass | ~40-60 bits (master password only) | 1 |
| Bitwarden | ~40-60 bits (master password only) | 1 |
| 1Password | password + 128-bit Secret Key | 2 |
| **Relicario** | **password + 256-bit image secret** | **2** |
| **Relicario** | **password + 256-bit second-factor secret (image or key file)** | **2** |
### What we don't protect against
- A compromised device with active malware. No software password manager can.
- Weak passphrases with a stolen reference image. Use 4+ diceware words.
- Weak passphrases with a stolen second factor (reference image or key file). Use 4+ diceware words.
- Rubber-hose cryptanalysis.
## Quick start
@@ -76,8 +78,10 @@ No single point of failure. The two-factor design means the passphrase alone can
# Build from source
cargo build --release
# Create a vault (pick any JPEG as the carrier)
# Create a vault with a reference image (default — stego second factor)
relicario init --image vacation.jpg --output reference.jpg
# Or use a plain key file as the second factor instead
relicario init --key-file /path/to/secret.relkey
# Add a credential
relicario add
@@ -94,7 +98,7 @@ relicario sync
# Pack the vault into a single encrypted backup file
relicario backup export -o vault.relbak
# Print a recovery QR for your image_secret (see "Recovery" below)
# Print a recovery QR for your second-factor secret (see "Recovery" below)
relicario recovery-qr generate
# Generate a random password
@@ -103,31 +107,35 @@ relicario generate -l 32
### Environment variable
Set `RELICARIO_IMAGE=/path/to/reference.jpg` to avoid being prompted for the image path on every command.
Set `RELICARIO_IMAGE=/path/to/reference.jpg` to avoid being prompted for the reference-image path on every command. If you use a key file instead, set `RELICARIO_KEYFILE=/path/to/secret.relkey`.
## The reference image
## The second factor: reference image and key file
The reference JPEG is generated once during `relicario init`. It looks like a normal photo — because it is one. The 256-bit secret is embedded in the DCT coefficients of the luminance channel using Quantization Index Modulation, with heavy redundancy and Reed-Solomon-style majority voting across multiple copies.
Relicario's second factor is a 32-byte secret. The **container** for that secret is your choice at vault creation.
The embedding survives:
**Reference image (default):** The JPEG carrier is generated during `relicario init`. It looks like a normal photo — because it is one. The 32-byte secret is embedded in the DCT coefficients of the luminance channel using Quantization Index Modulation, with heavy redundancy and Reed-Solomon-style majority voting across multiple copies.
The steganographic embedding survives:
- JPEG recompression (tested down to quality 85)
- Up to ~10-15% cropping from any edge
- Social media re-encoding (Instagram, Discord, etc.)
This means your reference image can live on your Instagram, your personal website, or anywhere else. It's useless without your passphrase.
This means your reference image can live on your Instagram, your personal website, or anywhere else — a "dead drop" approach. It is useless without your passphrase; the stego embedding is its transport mechanism.
## Recovery: what if I lose my reference image?
**Key file (alternative):** A plain armored `.relkey` file holding the same 32-byte secret in base64 — no steganography. Suited to users who prefer copying a small file to a USB drive, a hardware security module, or an offline storage medium. Wire format: see [docs/FORMATS.md](docs/FORMATS.md).
Without your reference image, the vault is undecryptable — that's the security model. But it also makes a lost or corrupted image a single point of failure.
## Recovery: what if I lose my second factor?
The mitigation is the **recovery QR**: a printable QR code that wraps your image secret behind a separate recovery passphrase you choose. If you ever lose access to the reference JPEG, scan or transcribe the QR, provide the recovery passphrase, and recover the 256-bit image secret. Combined with your normal vault passphrase, this restores access to the vault.
Without your second-factor container (reference image or key file), the vault is undecryptable — that's the security model. But it also makes a lost or corrupted container a single point of failure.
The mitigation is the **recovery QR**: a printable QR code that wraps the 32-byte second-factor secret behind a separate recovery passphrase you choose. It applies regardless of which container you chose at vault creation. If you ever lose access to your reference image or key file, scan or transcribe the QR, provide the recovery passphrase, and recover the second-factor secret. Combined with your normal vault passphrase, this restores access to the vault.
```bash
# Print a recovery QR (after the vault is unlocked).
# You'll be prompted for a separate recovery passphrase.
relicario recovery-qr generate
# Recover the image_secret from a stored QR payload.
# Recover the second-factor secret from a stored QR payload.
relicario recovery-qr unwrap
```
@@ -145,7 +153,7 @@ A short tour of the four codebases and how they fit together lives in [DESIGN.md
| Primitive | Purpose | Why this one |
|---|---|---|
| Argon2id (64 MiB, 3 iter, 4 parallel) | Key derivation from passphrase + image secret | Memory-hard, GPU-resistant, OWASP recommended |
| Argon2id (64 MiB, 3 iter, 4 parallel) | Key derivation from passphrase + second-factor secret | Memory-hard, GPU-resistant, OWASP recommended |
| XChaCha20-Poly1305 | Authenticated encryption of vault entries | 192-bit nonce (no collision risk), fast in WASM/ARM without AES-NI |
| ed25519 | Device key signing | Per-device commit authorization, revocable without KDF rotation |
@@ -182,7 +190,7 @@ Item IDs are random 16-char hex strings (64 bits of entropy). Git history is pre
Each device generates its own ed25519 keypair. The public key is stored in `.relicario/devices.json` (committed to the repo). Device keys are used for commit signing — they do NOT participate in vault decryption.
Revoking a device: remove its key from `devices.json` and commit. No passphrase or reference image rotation needed.
Revoking a device: remove its key from `devices.json` and commit. No passphrase or second-factor rotation needed.
```bash
relicario device add --name laptop
@@ -210,7 +218,7 @@ The binary is at `target/release/relicario`.
- [x] Typed items: Login, SecureNote, Identity, Card, Key, Document, TOTP
- [x] Secure document storage (encrypted file attachments)
- [x] Backup & restore (`.relbak` encrypted envelope)
- [x] Recovery QR (paper-printable image_secret backup with separate passphrase)
- [x] Recovery QR (paper-printable second-factor secret backup with separate passphrase)
- [x] LastPass CSV import
- [x] Device authentication (ed25519 commit signing + pre-receive hook)
- [ ] Import from Bitwarden / 1Password