From c50e0d448b09250986adc9d3facebba2c7089311 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 11 Apr 2026 23:22:50 -0400 Subject: [PATCH] docs: add architecture overview with flow diagrams --- docs/ARCHITECTURE.md | 254 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..ccafee0 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,254 @@ +# idfoto — Architecture + +## System Overview + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ CLIENT DEVICE (trusted) │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ Reference │ │ Passphrase │ │ idfoto-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) │ +│ │ +│ idfoto-vault.git/ │ +│ ├── manifest.enc ← opaque ciphertext │ +│ ├── entries/ │ +│ │ ├── a1b2c3d4.enc ← opaque ciphertext │ +│ │ └── e5f6a7b8.enc ← opaque ciphertext │ +│ └── .idfoto/ │ +│ ├── 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 + +``` +┌────────────────────────────────────────────────────────────┐ +│ idfoto-cli │ +│ Filesystem, git (shelling out), terminal I/O, clipboard │ +│ │ +│ Depends on: idfoto-core, clap, anyhow, rpassword, arboard │ +└──────────────────────┬─────────────────────────────────────┘ + │ uses + ▼ +┌────────────────────────────────────────────────────────────┐ +│ idfoto-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: idfoto-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) +```