docs: add README with security model and CLAUDE.md project context
This commit is contained in:
73
CLAUDE.md
Normal file
73
CLAUDE.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# CLAUDE.md — idfoto
|
||||||
|
|
||||||
|
## What is this
|
||||||
|
|
||||||
|
idfoto is a git-backed, self-hostable password manager with a Rust core. Two-factor vault decryption: passphrase + a reference JPEG carrying a 256-bit secret embedded via DCT steganography. The server only ever sees opaque ciphertext.
|
||||||
|
|
||||||
|
## Build and test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build # build everything
|
||||||
|
cargo test # run all tests (unit + integration)
|
||||||
|
cargo test -p idfoto-core # core library tests only
|
||||||
|
cargo run -- --help # CLI help
|
||||||
|
cargo run -- generate -l 32 # quick smoke test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
crates/
|
||||||
|
├── idfoto-core/ # Platform-agnostic library (no filesystem, no git, no network)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── lib.rs # Re-exports public API
|
||||||
|
│ │ ├── error.rs # IdfotoError enum (thiserror)
|
||||||
|
│ │ ├── crypto.rs # Argon2id KDF + XChaCha20-Poly1305 encrypt/decrypt
|
||||||
|
│ │ ├── entry.rs # Entry, ManifestEntry, Manifest structs (serde)
|
||||||
|
│ │ ├── vault.rs # encrypt_entry, decrypt_entry, encrypt_manifest, decrypt_manifest
|
||||||
|
│ │ └── imgsecret.rs # DCT-based 256-bit secret embedding in JPEGs
|
||||||
|
│ └── tests/
|
||||||
|
│ └── integration.rs # Full-workflow and two-factor independence tests
|
||||||
|
└── idfoto-cli/ # CLI binary
|
||||||
|
└── src/
|
||||||
|
└── main.rs # clap CLI: init, add, get, list, edit, rm, sync, generate, device
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key design decisions
|
||||||
|
|
||||||
|
- **idfoto-core is bytes-in/bytes-out.** No filesystem, no network, no git operations. Makes it portable to WASM, Android, iOS.
|
||||||
|
- **XChaCha20-Poly1305** over AES-GCM — 192-bit nonce eliminates collision risk, fast in WASM/ARM without AES-NI.
|
||||||
|
- **Single master_key** (no per-entry subkeys) — simpler, sufficient for family vault sizes.
|
||||||
|
- **imgsecret uses central-embed DCT** — embeds only in the middle 70% of the image (15% crumple zone for crop tolerance), with majority voting across 5-50 redundant copies.
|
||||||
|
- **QUANT_STEP = 50.0** — higher than typical (25) to survive JPEG recompression down to Q85.
|
||||||
|
- **Device ed25519 keys are separate from the KDF.** Revoking a device doesn't require rotating the passphrase or reference image.
|
||||||
|
|
||||||
|
## Crypto pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
passphrase (UTF-8 bytes) || image_secret (32 bytes from reference JPEG)
|
||||||
|
→ Argon2id(salt=vault_salt, m=64MiB, t=3, p=4)
|
||||||
|
→ master_key (32 bytes)
|
||||||
|
→ XChaCha20-Poly1305(nonce=random 24 bytes)
|
||||||
|
→ encrypted entry/manifest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Tests use fast Argon2id params (m=256, t=1, p=1) so they don't take forever.
|
||||||
|
- Test JPEGs are generated synthetically via `make_test_jpeg()` — no binary test fixtures.
|
||||||
|
- Entry IDs are random 8-char hex strings.
|
||||||
|
- Git history is preserved as an audit log — no squashing.
|
||||||
|
- The CLI shells out to `git` for sync — no libgit2/gitoxide dependency.
|
||||||
|
|
||||||
|
## Remote
|
||||||
|
|
||||||
|
Source code: `ssh://git@git.adlee.work:2222/alee/idfoto.git`
|
||||||
|
|
||||||
|
## Design spec
|
||||||
|
|
||||||
|
Full threat model, entropy analysis, and architecture: `docs/superpowers/specs/2026-04-11-idfoto-design.md`
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Next: WASM build + Chrome MV3 browser extension (Plan 2). Then mobile (Rust core compiles to ARM).
|
||||||
196
README.md
Normal file
196
README.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# idfoto
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The server only ever sees opaque ciphertext. There is nothing else going on. This README is the security proof.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
```
|
||||||
|
Your passphrase (something you know)
|
||||||
|
+
|
||||||
|
Your reference photo (something you have)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ Argon2id KDF ] --> master_key --> [ XChaCha20-Poly1305 ] --> encrypted vault
|
||||||
|
^ |
|
||||||
|
| v
|
||||||
|
Never leaves Stored in git
|
||||||
|
your device (opaque ciphertext)
|
||||||
|
```
|
||||||
|
|
||||||
|
At vault creation, idfoto 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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Security model
|
||||||
|
|
||||||
|
### What the server sees
|
||||||
|
|
||||||
|
A git repository containing:
|
||||||
|
- `manifest.enc` — opaque binary blob
|
||||||
|
- `entries/*.enc` — more opaque binary blobs
|
||||||
|
- `.idfoto/salt` — a random 32-byte value (not secret)
|
||||||
|
- `.idfoto/params.json` — Argon2id parameters (not secret)
|
||||||
|
- `.idfoto/devices.json` — authorized device public keys
|
||||||
|
|
||||||
|
That's it. No plaintext. No metadata about what's inside. No keys, no passphrases, no reference images.
|
||||||
|
|
||||||
|
### 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. |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Compared to
|
||||||
|
|
||||||
|
| | Server breach entropy | KDF factors |
|
||||||
|
|---|---|---|
|
||||||
|
| LastPass | ~40-60 bits (master password only) | 1 |
|
||||||
|
| Bitwarden | ~40-60 bits (master password only) | 1 |
|
||||||
|
| 1Password | password + 128-bit Secret Key | 2 |
|
||||||
|
| **idfoto** | **password + 256-bit image secret** | **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.
|
||||||
|
- Rubber-hose cryptanalysis.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build from source
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Create a vault (pick any JPEG as the carrier)
|
||||||
|
idfoto init --image vacation.jpg --output reference.jpg
|
||||||
|
|
||||||
|
# Add a credential
|
||||||
|
idfoto add
|
||||||
|
|
||||||
|
# Retrieve it
|
||||||
|
idfoto get github
|
||||||
|
|
||||||
|
# List everything
|
||||||
|
idfoto list
|
||||||
|
|
||||||
|
# Sync with your git remote
|
||||||
|
idfoto sync
|
||||||
|
|
||||||
|
# Generate a random password
|
||||||
|
idfoto generate -l 32
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variable
|
||||||
|
|
||||||
|
Set `IDFOTO_IMAGE=/path/to/reference.jpg` to avoid being prompted for the image path on every command.
|
||||||
|
|
||||||
|
## The reference image
|
||||||
|
|
||||||
|
The reference JPEG is generated once during `idfoto 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.
|
||||||
|
|
||||||
|
The 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.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
idfoto/
|
||||||
|
├── crates/
|
||||||
|
│ ├── idfoto-core/ # Platform-agnostic library (no filesystem, no network)
|
||||||
|
│ │ ├── crypto.rs # Argon2id KDF + XChaCha20-Poly1305 AEAD
|
||||||
|
│ │ ├── imgsecret.rs # DCT steganography: embed/extract 256-bit secrets in JPEGs
|
||||||
|
│ │ ├── entry.rs # Entry, Manifest data model (serde)
|
||||||
|
│ │ └── vault.rs # Encrypt/decrypt entries and manifests
|
||||||
|
│ └── idfoto-cli/ # CLI binary: filesystem, git, terminal I/O
|
||||||
|
└── docs/
|
||||||
|
└── superpowers/
|
||||||
|
└── specs/ # Design specification with full threat model
|
||||||
|
```
|
||||||
|
|
||||||
|
`idfoto-core` takes bytes and returns bytes. It has no knowledge of filesystems, git, or networks. This makes it portable to WASM (browser extension), Android (JNI), and iOS (Swift bridge).
|
||||||
|
|
||||||
|
### Crypto primitives
|
||||||
|
|
||||||
|
| Primitive | Purpose | Why this one |
|
||||||
|
|---|---|---|
|
||||||
|
| Argon2id (64 MiB, 3 iter, 4 parallel) | Key derivation from passphrase + image 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 |
|
||||||
|
|
||||||
|
### Encrypted file format
|
||||||
|
|
||||||
|
```
|
||||||
|
version (1 byte) | nonce (24 bytes) | ciphertext (variable) | auth tag (16 bytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
Every write generates a fresh random nonce. The version byte allows future format changes.
|
||||||
|
|
||||||
|
## Vault layout
|
||||||
|
|
||||||
|
```
|
||||||
|
my-vault.git/
|
||||||
|
├── manifest.enc # Encrypted entry index (names, URLs, timestamps)
|
||||||
|
├── entries/
|
||||||
|
│ ├── a1b2c3d4.enc # One encrypted entry per file
|
||||||
|
│ └── e5f6a7b8.enc
|
||||||
|
└── .idfoto/
|
||||||
|
├── salt # 32-byte random salt (not secret)
|
||||||
|
├── params.json # KDF parameters
|
||||||
|
└── devices.json # Authorized device public keys
|
||||||
|
```
|
||||||
|
|
||||||
|
Entry IDs are random hex strings. Git history is preserved — every add/edit/delete is a commit. "When was this password last rotated?" is answered by `git log`.
|
||||||
|
|
||||||
|
## Device management
|
||||||
|
|
||||||
|
Each device generates its own ed25519 keypair. The public key is stored in `.idfoto/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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idfoto device add --name laptop
|
||||||
|
idfoto device list
|
||||||
|
idfoto device revoke laptop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires Rust stable (1.70+).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@git.adlee.work:2222/alee/idfoto.git
|
||||||
|
cd idfoto
|
||||||
|
cargo build --release
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is at `target/release/idfoto`.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] WASM build + Chrome browser extension (inline crypto, no native messaging)
|
||||||
|
- [ ] Secure notes (free-form encrypted text entries)
|
||||||
|
- [ ] Secure document storage (encrypted file attachments up to 5-10 MB)
|
||||||
|
- [ ] `idfoto unlock` daemon (ssh-agent-style, holds master key for a TTL)
|
||||||
|
- [ ] Android/iOS clients (Rust core compiles to ARM)
|
||||||
|
- [ ] Import from LastPass/Bitwarden/1Password
|
||||||
|
- [ ] Firefox/Safari extensions
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built by [Aaron Lee](https://adlee.work). Design spec and threat model in `docs/superpowers/specs/`.
|
||||||
Reference in New Issue
Block a user