255 lines
16 KiB
Markdown
255 lines
16 KiB
Markdown
# 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)
|
||
```
|