The Vault Creation Flow ASCII showed only manifest.enc as init's encrypted artifact; cmd_init has been writing settings.enc in parallel since the VaultSettings rollout. Update the encrypt step to show both artifacts side-by-side with independent nonces. Below the ASCII, add a short pointer noting that the per-item lifecycle (typed-item envelope, attachment encryption, field-history) lives in crates/relicario-core/ARCHITECTURE.md and reuses the same master_key + XChaCha20-Poly1305 primitives. The doc-audit framing is "this top-level doc could just point at the per-crate docs" — taking that trim path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
270 lines
17 KiB
Markdown
270 lines
17 KiB
Markdown
# 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 │
|
||
│ ├── settings.enc ← opaque ciphertext │
|
||
│ ├── items/ │
|
||
│ │ ├── a1b2c3d4e5f6a7b8.enc ← opaque ciphertext │
|
||
│ │ └── … │
|
||
│ ├── attachments/ │
|
||
│ │ └── <item-id>/<aid>.enc ← opaque ciphertext │
|
||
│ └── .relicario/ │
|
||
│ ├── salt ← 32 bytes (not secret) │
|
||
│ ├── params.json ← KDF params (not secret) │
|
||
│ ├── devices.json ← device public keys (not secret) │
|
||
│ └── revoked.json ← revoked device records (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 │ settings.enc
|
||
default settings ──►│ encrypt (×2) │ (parallel artifacts;
|
||
└──────────────────┘ independent nonces)
|
||
|
||
┌──────────────────┐
|
||
│ git init │──────► vault repo
|
||
│ git commit │ (ready to push)
|
||
└──────────────────┘
|
||
```
|
||
|
||
Item creation, the typed-item envelope (`Item` + per-type `ItemCore`),
|
||
attachment encryption, and field-history tracking are not shown above —
|
||
they are described in [`crates/relicario-core/ARCHITECTURE.md`](../crates/relicario-core/ARCHITECTURE.md).
|
||
The flow above covers only the crypto-pipeline shape that vault init
|
||
establishes; the per-item lifecycle reuses the same `master_key` +
|
||
XChaCha20-Poly1305 primitives against `items/<id>.enc` and
|
||
`attachments/<item-id>/<aid>.enc`.
|
||
|
||
## 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│ │ item + │ │ vault │ │
|
||
│ │ │ │ │ │ types │ │ │ │
|
||
│ │ KDF │ │ DCT │ │ Item │ │ encrypt_ │ │
|
||
│ │ encrypt │ │ embed │ │ Manifest│ │ item() │ │
|
||
│ │ decrypt │ │ extract │ │ Settings│ │ decrypt_ │ │
|
||
│ │ │ │ QIM │ │ Backup │ │ manifest() │ │
|
||
│ │ │ │ │ │ Device │ │ ... │ │
|
||
│ └──────────┘ └──────────┘ └─────────┘ └────────────┘ │
|
||
│ │
|
||
│ Consumed by: relicario-cli, relicario-wasm (extension), │
|
||
│ relicario-server (pre-receive hook). │
|
||
│ 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)
|
||
```
|