# Relicario — Architecture ## System Overview ``` ┌──────────────────────────────────────────────────────────────────┐ │ CLIENT DEVICE (trusted) │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ Reference │ │ Passphrase │ │ relicario-cli │ │ │ │ JPEG │ │ (typed) │ │ or browser ext │ │ │ │ (on disk) │ │ │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌──────────────┐ │ │ │ │ │ imgsecret │ │ │ │ │ │ ::extract() │ │ │ │ │ └──────┬───────┘ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ ┌──────────────────────────────┐ │ │ │ │ Argon2id KDF │ │ │ │ │ password = passphrase ‖ │ │ │ │ │ image_secret │ │ │ │ │ salt = vault_salt │ │ │ │ │ → master_key (32 bytes) │ │ │ │ └──────────────┬───────────────┘ │ │ │ │ │ │ │ ▼ │ │ │ ┌──────────────────────────────┐ │ │ │ │ XChaCha20-Poly1305 │◄──────────────────┘ │ │ │ encrypt / decrypt │ │ │ │ (192-bit nonce, 256-bit │ │ │ │ key, 128-bit auth tag) │ │ │ └──────────────┬───────────────┘ │ │ │ │ └─────────────────┼──────────────────────────────────────────────────┘ │ │ git push / pull (HTTPS or SSH) ▼ ┌──────────────────────────────────────────────────────────────────┐ │ GIT SERVER (untrusted) │ │ │ │ relicario-vault.git/ │ │ ├── manifest.enc ← opaque ciphertext │ │ ├── entries/ │ │ │ ├── a1b2c3d4.enc ← opaque ciphertext │ │ │ └── e5f6a7b8.enc ← opaque ciphertext │ │ └── .relicario/ │ │ ├── salt ← 32 bytes (not secret) │ │ ├── params.json ← KDF params (not secret) │ │ └── devices.json ← device public keys (not secret) │ │ │ │ The server sees NOTHING useful. No keys, no plaintext, │ │ no metadata about what's inside. │ └──────────────────────────────────────────────────────────────────┘ ``` ## Vault Creation Flow ``` User provides: System generates: ├── carrier JPEG (any photo) ├── image_secret (256-bit random) └── passphrase (memorized) └── vault_salt (256-bit random) ┌──────────────────┐ carrier JPEG ──────►│ imgsecret │──────► reference.jpg image_secret ──────►│ ::embed() │ (looks like a normal photo, │ DCT stego │ carries hidden secret) └──────────────────┘ ┌──────────────────┐ passphrase ────────►│ │ │ Argon2id │──────► master_key (32 bytes) image_secret ──────►│ (64 MiB, 3 it) │ (held in memory only) vault_salt ────────►│ │ └──────────────────┘ ┌──────────────────┐ master_key ────────►│ XChaCha20- │──────► manifest.enc empty manifest ────►│ Poly1305 │ └──────────────────┘ ┌──────────────────┐ │ git init │──────► vault repo │ git commit │ (ready to push) └──────────────────┘ ``` ## Unlock Flow (every vault operation) ``` ┌──────────────────┐ reference.jpg ─────►│ imgsecret │──────► image_secret │ ::extract() │ (256 bits) └──────────────────┘ ┌──────────────────┐ passphrase ────────►│ │ image_secret ──────►│ Argon2id │──────► master_key vault_salt ────────►│ │ └──────────────────┘ ┌──────────────────┐ master_key ────────►│ XChaCha20 │──────► plaintext entries *.enc files ───────►│ ::decrypt() │ (in memory only) └──────────────────┘ ``` ## imgsecret DCT Embedding ``` Input JPEG │ ▼ ┌──────────────────┐ │ Decode to RGB │ │ Extract Y │ │ (luminance) │ └────────┬─────────┘ │ ▼ ┌──────────────────────────────┐ │ Full Image │ │ ┌────────────────────────┐ │ │ │ 15% ┌────────────┐ │ │ │ │margin│ │15% │ │ │ │ │ Central │mar-│ │ │ │ │ 70% │gin │ │ │ │ │ EMBEDDING │ │ │ │ │ │ REGION │ │ │ │ │ └────────────┘ │ │ │ │ 15% margin │ │ │ └────────────────────────┘ │ └──────────────────────────────┘ │ ▼ ┌──────────────────┐ │ Divide into │ │ 8×8 blocks │ │ Apply 2D DCT │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ For each │ │ selected block: │ │ │ │ QIM embed bits │ │ in positions │ │ 4-15 (mid-freq) │ │ │ │ Repeat secret │ │ 20+ times │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Inverse DCT │ │ Reconstruct RGB │ │ Save JPEG (Q92) │ └──────────────────┘ │ ▼ Output: reference.jpg (visually identical, carries 256-bit secret) ``` ## Extraction (with crop recovery) ``` Input JPEG (possibly re-encoded or cropped) │ ▼ ┌─────────────────────────┐ │ Try canonical alignment │──── Success ──► image_secret │ (offset 0,0) │ └─────────┬───────────────┘ │ Fail ▼ ┌─────────────────────────┐ │ Search crop offsets │ │ dx, dy: -15% to +15% │ │ step: 8 pixels │ │ (~16,800 candidates) │──── Success ──► image_secret │ │ │ For each: extract bits, │ │ majority vote, check │ │ confidence ≥ 60% │ └─────────┬───────────────┘ │ All fail ▼ ExtractionFailed error ``` ## Encrypted File Format ``` ┌─────────┬────────────────────────┬──────────────────┬──────────────────┐ │ version │ nonce │ ciphertext │ auth tag │ │ 1 byte │ 24 bytes │ N bytes │ 16 bytes │ │ 0x01 │ random per write │ XChaCha20 stream │ Poly1305 MAC │ └─────────┴────────────────────────┴──────────────────┴──────────────────┘ ``` ## Crate Architecture ``` ┌────────────────────────────────────────────────────────────┐ │ relicario-cli │ │ Filesystem, git (shelling out), terminal I/O, clipboard │ │ │ │ Depends on: relicario-core, clap, anyhow, rpassword, arboard │ └──────────────────────┬─────────────────────────────────────┘ │ uses ▼ ┌────────────────────────────────────────────────────────────┐ │ relicario-core │ │ Platform-agnostic: bytes in, bytes out │ │ No filesystem, no network, no git │ │ │ │ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌────────────┐ │ │ │ crypto │ │ imgsecret│ │ entry │ │ vault │ │ │ │ │ │ │ │ │ │ │ │ │ │ KDF │ │ DCT │ │ Entry │ │ encrypt_ │ │ │ │ encrypt │ │ embed │ │ Manifest│ │ entry() │ │ │ │ decrypt │ │ extract │ │ search │ │ decrypt_ │ │ │ │ │ │ QIM │ │ │ │ manifest() │ │ │ └──────────┘ └──────────┘ └─────────┘ └────────────┘ │ │ │ │ Future: relicario-wasm wraps this for browser extension │ │ Future: JNI/Swift wrappers for Android/iOS │ └────────────────────────────────────────────────────────────┘ ``` ## Entropy at Each Attack Scenario ``` Server breach only: ████████████████████████████ 256+ bits (infeasible) passphrase + image_secret Server + stolen image: ████░░░░░░░░░░░░░░░░░░░░░░ ~51 bits (4 diceware words) passphrase through Argon2id ~7 million years Shoulder-surfed passphrase: ████████████████████████████ 256 bits (infeasible) image_secret Stolen device: ████░░░░░░░░░░░░░░░░░░░░░░ ~51 bits (4 diceware words) passphrase through Argon2id ~7 million years Both factors compromised: game over (same as every password manager) ```