chore: rename project from idfoto to relicario

Sweeping rename across crates, CLI binary, WASM bindings, extension, docs,
and vault metadata paths. Git remote updated to relicario.git.

- crates/idfoto-{core,cli,wasm} -> crates/relicario-{core,cli,wasm}
- IdfotoError -> RelicarioError
- IDFOTO_IMAGE env var -> RELICARIO_IMAGE
- ~/.config/idfoto -> ~/.config/relicario
- .idfoto/ vault metadata dir -> .relicario/ (breaking; pre-release)
- Binary name idfoto -> relicario
- Extension wasm module idfoto_wasm -> relicario_wasm
- Storage key idfotoSettings -> relicarioSettings
- All doc filenames and content references updated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-19 16:47:02 -04:00
parent 20ff1d9f47
commit 519a6f0e36
51 changed files with 949 additions and 949 deletions

View File

@@ -1,10 +1,10 @@
# idfoto — Design Specification
# relicario — Design Specification
A git-backed, self-hostable password manager with a Rust core, CLI, and Chrome browser extension. The reference image as a DCT-embedded secret carrier is the core differentiator.
## Overview
idfoto is a password manager where vault decryption requires two independent factors: a passphrase the user memorizes and a reference JPEG that carries a 256-bit secret embedded via DCT steganography. The vault lives in a git repository (self-hosted on the user's own Gitea instance), and the server only ever sees opaque ciphertext. Compromise of either factor alone is insufficient to decrypt the vault.
relicario is a password manager where vault decryption requires two independent factors: a passphrase the user memorizes and a reference JPEG that carries a 256-bit secret embedded via DCT steganography. The vault lives in a git repository (self-hosted on the user's own Gitea instance), and the server only ever sees opaque ciphertext. Compromise of either factor alone is insufficient to decrypt the vault.
Primary goals: portfolio project for adlee.work, architectural elegance, legibility-as-security (the README should read as the security proof), learning Rust, and fun to tinker with.
@@ -23,7 +23,7 @@ A collection of credentials (usernames, passwords, URLs, TOTP seeds, notes) belo
| Stolen device | Filesystem: reference image, device key, cached vault | Decrypt vault | Attacker has image_secret but not passphrase. Argon2id makes brute-force expensive. |
| Stolen device + weak passphrase | Same + feasible brute-force | Decrypt vault | Enforce minimum passphrase strength at vault creation. Universal worst case. |
| Shoulder surfer | Observed passphrase | Decrypt vault (if they also get image) | Passphrase alone insufficient — still need image_secret. |
| Credential stuffing | Leaked email/password from other breaches | Access user's accounts | idfoto generates unique passwords per site. Breach of site A doesn't compromise site B. |
| Credential stuffing | Leaked email/password from other breaches | Access user's accounts | relicario generates unique passwords per site. Breach of site A doesn't compromise site B. |
### Out of scope
@@ -50,7 +50,7 @@ passphrase (user types, UTF-8 encoded)
Argon2id(
password = passphrase_bytes || image_secret_bytes, // concatenated, 32-byte secret appended
salt = vault_salt, // 32 bytes, from .idfoto/salt
salt = vault_salt, // 32 bytes, from .relicario/salt
memory = 64 MiB,
iterations = 3,
parallelism = 4,
@@ -79,7 +79,7 @@ With a 4-word diceware passphrase (~51 bits) and Argon2id at 64 MiB, brute-force
Compared to competitors:
- LastPass/Bitwarden: server breach exposes ~40-60 bits (master password only)
- 1Password: server breach exposes password + 128-bit Secret Key
- idfoto: server breach exposes password + 256-bit image_secret
- relicario: server breach exposes password + 256-bit image_secret
### Authenticated encryption
@@ -103,7 +103,7 @@ Nonce is generated fresh (CSPRNG) on every write. Version byte allows future for
### KDF parameters
Stored in `.idfoto/params.json` (plaintext, committed). Configurable per-vault:
Stored in `.relicario/params.json` (plaintext, committed). Configurable per-vault:
- Default: `argon2_m=65536` (64 MiB), `argon2_t=3`, `argon2_p=4`
- Users can increase for CLI-only use on powerful hardware
- Enables future parameter upgrades without format changes
@@ -177,13 +177,13 @@ Caller must normalize EXIF orientation before passing JPEG to embed/extract. EXI
## Vault Format & Repo Layout
```
idfoto-vault/
relicario-vault/
├── manifest.enc # encrypted JSON: entry index, vault metadata
├── entries/
│ ├── a1b2c3d4.enc # one encrypted entry per file, random hex ID
│ ├── e5f6a7b8.enc
│ └── ...
└── .idfoto/
└── .relicario/
├── salt # 32 bytes, plaintext (prevents precomputation)
├── params.json # Argon2id parameters, plaintext
└── devices.json # authorized device ed25519 public keys, plaintext
@@ -226,7 +226,7 @@ Flat schema. No nested objects, no folders, no tags for V1. Entry IDs are random
### Plaintext metadata
Stored in `.idfoto/` and committed to the repo:
Stored in `.relicario/` and committed to the repo:
- `salt`: 32 random bytes, generated once at vault creation
- `params.json`: Argon2id tuning knobs (memory, iterations, parallelism, format version)
- `devices.json`: list of authorized device ed25519 public keys, used to verify commit signatures
@@ -244,20 +244,20 @@ Preserved as-is. Every add/edit/rm is a commit. Provides "when was this password
## Crate Layout
```
idfoto/
relicario/
├── Cargo.toml # workspace root
├── crates/
│ ├── idfoto-core/ # library: imgsecret, KDF, vault format
│ ├── relicario-core/ # library: imgsecret, KDF, vault format
│ │ └── src/
│ │ ├── lib.rs
│ │ ├── imgsecret.rs
│ │ ├── kdf.rs
│ │ ├── vault.rs
│ │ └── entry.rs
│ ├── idfoto-cli/ # binary: the `idfoto` CLI
│ ├── relicario-cli/ # binary: the `relicario` CLI
│ │ └── src/
│ │ └── main.rs
│ └── idfoto-wasm/ # wasm-bindgen wrapper around core
│ └── relicario-wasm/ # wasm-bindgen wrapper around core
│ └── src/
│ └── lib.rs
├── extension/ # TypeScript Chrome MV3 extension
@@ -271,14 +271,14 @@ idfoto/
### Design principles
- **`idfoto-core` is platform-agnostic.** No filesystem, no git, no network. Takes bytes, returns bytes. This makes it trivially portable to WASM, Android (via JNI), iOS (via Swift bridge).
- **`idfoto-cli`** is the platform layer. Handles filesystem, git operations (shells out to `git`), clipboard, terminal I/O.
- **`idfoto-wasm`** is a thin wasm-bindgen wrapper exposing core functions to JavaScript.
- **`relicario-core` is platform-agnostic.** No filesystem, no git, no network. Takes bytes, returns bytes. This makes it trivially portable to WASM, Android (via JNI), iOS (via Swift bridge).
- **`relicario-cli`** is the platform layer. Handles filesystem, git operations (shells out to `git`), clipboard, terminal I/O.
- **`relicario-wasm`** is a thin wasm-bindgen wrapper exposing core functions to JavaScript.
- **`extension/`** is TypeScript/MV3. Loads the WASM module, runs crypto inline (no native messaging bridge).
### Rust crate dependencies (expected)
**idfoto-core:**
**relicario-core:**
- `argon2` — Argon2id KDF
- `chacha20poly1305` — XChaCha20-Poly1305 AEAD
- `sha2` — SHA-256 for hashing
@@ -290,44 +290,44 @@ idfoto/
- `ed25519-dalek` — device key signing (used by CLI, exposed via core)
- `thiserror` — error types
**idfoto-cli:**
**relicario-cli:**
- `clap` (derive) — argument parsing
- `anyhow` — CLI error handling
- `rpassword` — passphrase prompt without echo
- `arboard` or `cli-clipboard` — clipboard access
- `dirs` — platform config/data directories
**idfoto-wasm:**
**relicario-wasm:**
- `wasm-bindgen` — JS interop
- `js-sys`, `web-sys` — browser APIs
## CLI Commands
```
idfoto init # Create vault: generate salt, prompt for passphrase,
relicario init # Create vault: generate salt, prompt for passphrase,
# prompt for carrier image, embed image_secret,
# output reference JPEG, git init + first commit
idfoto add # Prompt for entry fields, encrypt, commit
idfoto get <name> # Case-insensitive substring match on name/URL, decrypt, copy password to clipboard (30s TTL)
idfoto list # Decrypt manifest, print entry names/URLs
idfoto edit <name> # Decrypt entry, prompt for changes, re-encrypt, commit
idfoto rm <name> # Remove entry file, update manifest, commit
idfoto sync # git pull --rebase && git push
idfoto generate # Generate a random password (utility, no vault interaction)
relicario add # Prompt for entry fields, encrypt, commit
relicario get <name> # Case-insensitive substring match on name/URL, decrypt, copy password to clipboard (30s TTL)
relicario list # Decrypt manifest, print entry names/URLs
relicario edit <name> # Decrypt entry, prompt for changes, re-encrypt, commit
relicario rm <name> # Remove entry file, update manifest, commit
relicario sync # git pull --rebase && git push
relicario generate # Generate a random password (utility, no vault interaction)
idfoto device add # Generate ed25519 keypair, add pubkey to devices.json, commit
idfoto device list # List authorized devices
idfoto device revoke <name> # Remove device from devices.json, commit
relicario device add # Generate ed25519 keypair, add pubkey to devices.json, commit
relicario device list # List authorized devices
relicario device revoke <name> # Remove device from devices.json, commit
```
Unlock flow: on any command that needs the vault, the CLI prompts for the passphrase and the reference image path (or uses a configured default path). Derives master_key, holds it in memory for the duration of the command, then drops it. No persistent daemon for V1 — each invocation re-derives.
Future: `idfoto unlock` could spawn a background agent (ssh-agent-style) that holds the key for a configurable TTL, so subsequent commands don't re-prompt.
Future: `relicario unlock` could spawn a background agent (ssh-agent-style) that holds the key for a configurable TTL, so subsequent commands don't re-prompt.
## Chrome Extension Architecture
The Chrome MV3 extension loads `idfoto-wasm` directly — no native messaging bridge.
The Chrome MV3 extension loads `relicario-wasm` directly — no native messaging bridge.
- **Service worker:** initializes the WASM module, holds the master_key in memory after unlock, handles vault operations
- **Popup:** passphrase prompt, entry list/search, entry detail view
@@ -343,16 +343,16 @@ Extension design details (popup UI, content script heuristics, autofill flow) ar
Not in V1 scope. Planned approach:
- `idfoto export-recovery` generates a small encrypted file containing only the `image_secret` (32 bytes + metadata), locked with the passphrase alone (separate Argon2id derivation)
- `relicario export-recovery` generates a small encrypted file containing only the `image_secret` (32 bytes + metadata), locked with the passphrase alone (separate Argon2id derivation)
- User stores this file offline (USB drive, printed QR, safe deposit box)
- Recovery: `idfoto recover --file recovery.enc` + passphrase → recovers image_secret → can decrypt vault from git
- Recovery: `relicario recover --file recovery.enc` + passphrase → recovers image_secret → can decrypt vault from git
- This is a second backup path alongside the "dead drop" reference JPEG (which can live on social media, personal website, etc.)
## Post-V1 Ideas
- **Secure notes:** free-form encrypted text entries (no URL/username/password schema, just a title + body). Same encryption, same repo layout — just a different entry type field.
- **Secure document storage:** encrypted file attachments up to 5-10 MB per entry. Stored as separate `.enc` blobs in an `attachments/` directory, referenced by entry ID. Git handles large binary blobs tolerably at this scale; git-lfs is an option if vaults grow beyond ~100 MB total.
- **`idfoto unlock` daemon:** ssh-agent-style background process that holds master_key for a configurable TTL, so repeated CLI commands don't re-prompt for passphrase.
- **`relicario unlock` daemon:** ssh-agent-style background process that holds master_key for a configurable TTL, so repeated CLI commands don't re-prompt for passphrase.
- **Mobile clients (Android/iOS):** Rust core compiles to ARM. Thin native wrappers (Kotlin/Swift) deferred.
- **Import from LastPass/Bitwarden/1Password**
- **Firefox/Safari extensions**

View File

@@ -1,4 +1,4 @@
# idfoto — Credential Capture Design
# relicario — Credential Capture Design
Experimental feature that detects login form submissions and prompts the user to save or update credentials in the vault. Configurable prompt style (notification bar or toast). Off by default.
@@ -60,7 +60,7 @@ A fixed-position bar at the top of the page, injected into the DOM:
```
┌──────────────────────────────────────────────────────────────────┐
idfoto: Save login for github.com? (alee) [Save] [Never] [✕] │
relicario: Save login for github.com? (alee) [Save] [Never] [✕] │
└──────────────────────────────────────────────────────────────────┘
```
@@ -77,7 +77,7 @@ A floating element in the bottom-right corner:
```
┌─────────────────────────────────┐
idfoto │
relicario │
│ Save login for github.com? │
│ alee │
│ [Save] [Never] [✕] │
@@ -103,7 +103,7 @@ When user clicks:
Stored in `chrome.storage.local` under key `settings`:
```typescript
interface IdfotoSettings {
interface RelicarioSettings {
captureEnabled: boolean; // default: false
captureStyle: 'bar' | 'toast'; // default: 'bar'
}
@@ -138,7 +138,7 @@ The toggle and style selector write to `chrome.storage.local`. Blacklist entries
| { type: 'check_credential'; url: string; username: string; password: string }
| { type: 'blacklist_site'; hostname: string }
| { type: 'get_settings' }
| { type: 'update_settings'; settings: Partial<IdfotoSettings> }
| { type: 'update_settings'; settings: Partial<RelicarioSettings> }
| { type: 'get_blacklist' }
| { type: 'remove_blacklist'; hostname: string }
@@ -161,7 +161,7 @@ extension/src/popup/components/settings.ts # Settings view
extension/src/content/detector.ts # Import and init capture module
extension/src/service-worker/index.ts # Handle new message types
extension/src/shared/messages.ts # Add new Request/Response types
extension/src/shared/types.ts # Add IdfotoSettings interface
extension/src/shared/types.ts # Add RelicarioSettings interface
extension/src/popup/popup.ts # Add 'settings' view to state machine
extension/src/popup/components/unlock.ts # Wire up settings button
```

View File

@@ -1,4 +1,4 @@
# idfoto — Firefox Extension Port Design
# relicario — Firefox Extension Port Design
Port the existing Chrome MV3 extension to Firefox. Shared TypeScript source, separate manifests, separate build outputs. No code changes to components, popup, or content script.
@@ -40,12 +40,12 @@ Firefox supports the `chrome.*` namespace for WebExtension APIs, so no `browser.
```json
{
"manifest_version": 3,
"name": "idfoto",
"name": "relicario",
"version": "0.1.0",
"description": "Two-factor encrypted password manager",
"browser_specific_settings": {
"gecko": {
"id": "idfoto@adlee.work",
"id": "relicario@adlee.work",
"strict_min_version": "128.0"
}
},
@@ -71,7 +71,7 @@ Firefox supports the `chrome.*` namespace for WebExtension APIs, so no `browser.
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"web_accessible_resources": [{
"resources": ["setup.html", "setup.js", "styles.css", "idfoto_wasm_bg.wasm", "idfoto_wasm.js"]
"resources": ["setup.html", "setup.js", "styles.css", "relicario_wasm_bg.wasm", "relicario_wasm.js"]
}]
}
```
@@ -94,12 +94,12 @@ async function initWasm(): Promise<WasmModule> {
if (typeof ServiceWorkerGlobalScope !== 'undefined') {
// Chrome MV3: service worker context — use initSync
const wasmResponse = await fetch(chrome.runtime.getURL('idfoto_wasm_bg.wasm'));
const wasmResponse = await fetch(chrome.runtime.getURL('relicario_wasm_bg.wasm'));
const wasmBytes = await wasmResponse.arrayBuffer();
initSync({ module: new WebAssembly.Module(wasmBytes) });
} else {
// Firefox: background script context — dynamic import works
const wasmUrl = chrome.runtime.getURL('idfoto_wasm_bg.wasm');
const wasmUrl = chrome.runtime.getURL('relicario_wasm_bg.wasm');
await initDefault(wasmUrl);
}
@@ -130,7 +130,7 @@ Identical to `webpack.config.js` except:
"build:all": "npm run build:wasm && npm run build && npm run build:firefox",
"dev": "webpack --mode development --watch",
"dev:firefox": "webpack --config webpack.firefox.config.js --mode development --watch",
"build:wasm": "wasm-pack build ../crates/idfoto-wasm --target web --out-dir ../../extension/wasm"
"build:wasm": "wasm-pack build ../crates/relicario-wasm --target web --out-dir ../../extension/wasm"
}
}
```

View File

@@ -1,6 +1,6 @@
# idfoto — Standalone Vault Initialization Wizard Design
# relicario — Standalone Vault Initialization Wizard Design
A browser-based wizard that guides new users through creating an idfoto vault from scratch. Lives at `extension/setup.html`, uses the same WASM module as the extension, same terminal dark aesthetic. No server, no Rust toolchain required.
A browser-based wizard that guides new users through creating an relicario vault from scratch. Lives at `extension/setup.html`, uses the same WASM module as the extension, same terminal dark aesthetic. No server, no Rust toolchain required.
## Scope
@@ -37,7 +37,7 @@ Step includes a "Next" button. No validation needed at this step.
Fields:
- Host URL (e.g. `https://git.adlee.work` or `https://github.com`) — pre-filled based on host type selection
- Repository path (e.g. `alee/idfoto-vault`)
- Repository path (e.g. `alee/relicario-vault`)
- API token (password field)
"Test Connection" button:
@@ -58,15 +58,15 @@ Two inputs:
1. Load WASM module
2. Generate random 32-byte `image_secret` via `crypto.getRandomValues()`
3. Embed secret into carrier JPEG via WASM `extract_image_secret` — wait, that's extract. We need `embed`. Check: the WASM crate currently only exposes `extract_image_secret`, not `embed`. **We need to add a `embed_image_secret` function to `idfoto-wasm`.**
3. Embed secret into carrier JPEG via WASM `extract_image_secret` — wait, that's extract. We need `embed`. Check: the WASM crate currently only exposes `extract_image_secret`, not `embed`. **We need to add a `embed_image_secret` function to `relicario-wasm`.**
4. Generate random 32-byte `salt` via `crypto.getRandomValues()`
5. Create `params.json` with default KDF params (`{"argon2_m":65536,"argon2_t":3,"argon2_p":4}`)
6. Derive `master_key` via WASM `derive_master_key(passphrase, image_secret, salt, params_json)`
7. Encrypt empty manifest (`{"entries":{},"version":1}`) via WASM `encrypt_manifest`
8. Push files to repo via git API:
- `.idfoto/salt` (raw 32 bytes)
- `.idfoto/params.json` (JSON string)
- `.idfoto/devices.json` (`[]`)
- `.relicario/salt` (raw 32 bytes)
- `.relicario/params.json` (JSON string)
- `.relicario/devices.json` (`[]`)
- `manifest.enc` (encrypted manifest bytes)
9. Show progress bar during push operations
@@ -81,20 +81,20 @@ Two things happen:
- Show warning: "Keep this image safe. You need it alongside your passphrase to unlock the vault. Store it somewhere you won't lose it."
**Push config to extension (if available):**
- Try to detect the idfoto extension via `chrome.runtime.sendMessage` with a `get_setup_state` message
- Try to detect the relicario extension via `chrome.runtime.sendMessage` with a `get_setup_state` message
- If extension responds: push `save_setup` message with `{ config: { hostType, hostUrl, repoPath, apiToken }, imageBase64 }`. Show "Extension configured! You can now open the extension and unlock your vault."
- If extension not detected: show the config as a copyable JSON blob with instructions: "Install the idfoto extension, then paste this into the setup wizard." (Or just tell them to run through the extension setup manually with the same host/token/repo.)
- If extension not detected: show the config as a copyable JSON blob with instructions: "Install the relicario extension, then paste this into the setup wizard." (Or just tell them to run through the extension setup manually with the same host/token/repo.)
## WASM Crate Change
The `idfoto-wasm` crate needs one new function:
The `relicario-wasm` crate needs one new function:
```rust
#[wasm_bindgen]
pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>, JsValue>
```
This wraps `idfoto_core::imgsecret::embed`. Currently only `extract_image_secret` is exposed.
This wraps `relicario_core::imgsecret::embed`. Currently only `extract_image_secret` is exposed.
## File Structure
@@ -154,7 +154,7 @@ Add `setup.html` to the extension so it can be opened as a chrome-extension page
```json
{
"web_accessible_resources": [{
"resources": ["setup.html", "setup.js", "styles.css", "idfoto_wasm_bg.wasm", "idfoto_wasm.js"],
"resources": ["setup.html", "setup.js", "styles.css", "relicario_wasm_bg.wasm", "relicario_wasm.js"],
"matches": ["<all_urls>"]
}]
}

View File

@@ -1,10 +1,10 @@
# idfoto — WASM + Chrome MV3 Extension Design
# relicario — WASM + Chrome MV3 Extension Design
The browser extension for idfoto. Compiles `idfoto-core` to WASM, wraps it in a Chrome MV3 extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access. No CLI dependency, no native messaging bridge.
The browser extension for relicario. Compiles `relicario-core` to WASM, wraps it in a Chrome MV3 extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access. No CLI dependency, no native messaging bridge.
## Scope
- `idfoto-wasm` crate — wasm-bindgen wrapper around `idfoto-core`
- `relicario-wasm` crate — wasm-bindgen wrapper around `relicario-core`
- Chrome MV3 extension:
- One-time setup wizard (git host + token + repo + reference image)
- Service worker — WASM runtime, master_key holder, vault operations, git API
@@ -44,9 +44,9 @@ pub struct ManifestEntry {
The `group` field is a free-form string. No predefined list, no nesting. User types "work" or "family" and entries cluster. Backwards-compatible — existing vaults without `group` deserialize as `None` (ungrouped).
## WASM Crate (`idfoto-wasm`)
## WASM Crate (`relicario-wasm`)
Thin wasm-bindgen wrapper exposing `idfoto-core` functions to JavaScript. Lives at `crates/idfoto-wasm/`.
Thin wasm-bindgen wrapper exposing `relicario-core` functions to JavaScript. Lives at `crates/relicario-wasm/`.
### Public API
@@ -94,7 +94,7 @@ pub fn generate_entry_id() -> String
```toml
[dependencies]
idfoto-core = { path = "../idfoto-core" }
relicario-core = { path = "../relicario-core" }
wasm-bindgen = "0.2"
js-sys = "0.3"
serde_json = "1"
@@ -106,10 +106,10 @@ data-encoding = "2" # base32 decoding for TOTP secrets
### WASM build
```bash
wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm
wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm
```
Output: `idfoto_wasm.js` (JS glue) + `idfoto_wasm_bg.wasm` (binary). Expected size ~200-500 KB gzipped. The `image` crate's JPEG decoder is the heaviest component — optimize only if measured size is a problem.
Output: `relicario_wasm.js` (JS glue) + `relicario_wasm_bg.wasm` (binary). Expected size ~200-500 KB gzipped. The `image` crate's JPEG decoder is the heaviest component — optimize only if measured size is a problem.
### TOTP implementation
@@ -147,7 +147,7 @@ interface WorkerState {
interface VaultConfig {
hostType: "gitea" | "github";
hostUrl: string; // e.g. "https://git.adlee.work"
repoPath: string; // e.g. "alee/idfoto-vault"
repoPath: string; // e.g. "alee/relicario-vault"
apiToken: string; // personal access token
imageBytes: Uint8Array; // reference JPEG, stored in chrome.storage.local
}
@@ -190,7 +190,7 @@ Popup and content script communicate with the service worker via typed messages:
2. Popup sends `{ type: "unlock", passphrase }` to service worker
3. Service worker loads vault config from `chrome.storage.local` (includes image bytes)
4. WASM: `extract_image_secret(image_bytes)``image_secret`
5. Service worker fetches `.idfoto/salt` and `.idfoto/params.json` via git API
5. Service worker fetches `.relicario/salt` and `.relicario/params.json` via git API
6. WASM: `derive_master_key(passphrase, image_secret, salt, params)``master_key`
7. Service worker fetches `manifest.enc` via git API
8. WASM: `decrypt_manifest(manifest_enc, master_key)` → manifest
@@ -330,7 +330,7 @@ No shadow DOM traversal. No heuristic scoring. No iframe inspection. If the form
### 2. Field Icon Injection
When a password field is detected:
- Small idfoto icon (16x16, inline SVG) appears at the right edge of the password field
- Small relicario icon (16x16, inline SVG) appears at the right edge of the password field
- Click triggers: send page URL to service worker → get matching entries
- Single match: fill immediately
- Multiple matches: show inline picker (small dropdown below the icon)
@@ -380,7 +380,7 @@ extension/
│ └── shared/
│ ├── messages.ts # typed message definitions
│ └── types.ts # Entry, ManifestEntry, VaultConfig, etc.
├── wasm/ # wasm-pack output (idfoto_wasm.js + .wasm)
├── wasm/ # wasm-pack output (relicario_wasm.js + .wasm)
├── icons/ # extension icons (16, 48, 128px)
└── dist/ # build output → load unpacked into Chrome
```
@@ -392,7 +392,7 @@ No framework. Vanilla TypeScript + DOM manipulation. The popup is small enough t
### WASM build
```bash
wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm
wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm
```
### Extension build
@@ -414,7 +414,7 @@ Chains wasm-pack then webpack. Dev mode: `npm run dev` watches TypeScript and au
```json
{
"manifest_version": 3,
"name": "idfoto",
"name": "relicario",
"version": "0.1.0",
"description": "Two-factor encrypted password manager",
"permissions": ["storage", "activeTab", "clipboardWrite"],
@@ -476,7 +476,7 @@ The token is stored in `chrome.storage.local`, which is sandboxed per-extension
- Unit tests: each wrapper function round-trips correctly (`wasm-pack test --node`)
- TOTP: test vectors from RFC 6238 appendix B
- Integration: derive key + encrypt + decrypt cycle matches `idfoto-core` output
- Integration: derive key + encrypt + decrypt cycle matches `relicario-core` output
### Extension (manual for V1)

View File

@@ -1,6 +1,6 @@
# idfoto — Typed Item Data Model Design
# relicario — Typed Item Data Model Design
Foundational data-model rewrite for idfoto. Replaces the single `Entry` type with a polymorphic typed-item system supporting Login, SecureNote, Identity, Card, Key, Document, and TOTP — with sections, custom fields, attachments, password history, soft-delete, and the security architecture needed to support 1Password-style daily-driver UX.
Foundational data-model rewrite for relicario. Replaces the single `Entry` type with a polymorphic typed-item system supporting Login, SecureNote, Identity, Card, Key, Document, and TOTP — with sections, custom fields, attachments, password history, soft-delete, and the security architecture needed to support 1Password-style daily-driver UX.
This is **Phase 1** of the broader 1Password-parity roadmap. Phase 0 (audit remediation) is the precursor implementation pass; Phase 2+ (admin portal, importers, Watchtower checks, etc.) build on top of this model.
@@ -8,7 +8,7 @@ This is **Phase 1** of the broader 1Password-parity roadmap. Phase 0 (audit reme
In:
- New typed-item Rust data model in `idfoto-core` (replaces `Entry`)
- New typed-item Rust data model in `relicario-core` (replaces `Entry`)
- New on-disk repo layout (items + attachments split, settings file, format version 2)
- Cryptographic envelope updates (length-prefixed Argon2 inputs, Zeroize discipline, opaque session-handle WASM bridge)
- Security architecture for the extension boundary (split message router, origin-checked autofill, closed Shadow DOM rendering, hardened CLI git shell-out)
@@ -69,7 +69,7 @@ Captured during brainstorming so the rationale is preserved:
```
┌────────────────────────────────────────────────────────────────────┐
idfoto-core (Rust) │
relicario-core (Rust) │
│ - Item, ItemCore (7 variants), Field, Section, Attachment │
│ - Manifest, VaultSettings │
│ - crypto: KDF (length-prefixed), AEAD, Zeroize discipline │
@@ -79,7 +79,7 @@ Captured during brainstorming so the rationale is preserved:
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────────┐
idfoto-cli (Rust) │ │ idfoto-wasm (Rust → WASM) │
relicario-cli (Rust) │ │ relicario-wasm (Rust → WASM) │
│ - clap commands │ │ - opaque session handles │
│ - hardened git │ │ - typed-item API surface │
│ - rpassword 7.x │ │ - master_key never returned to │
@@ -141,7 +141,7 @@ pub enum ItemCore {
}
```
Each variant struct lives in `crates/idfoto-core/src/item_types/<type>.rs`. Compiler enforces exhaustiveness across the codebase — adding a new variant later means: create the file, add the enum variant, fix the (typically ~5) match-arm sites the compiler points at, register the UI form. No reflection, no registry, no runtime dispatch.
Each variant struct lives in `crates/relicario-core/src/item_types/<type>.rs`. Compiler enforces exhaustiveness across the codebase — adding a new variant later means: create the file, add the enum variant, fix the (typically ~5) match-arm sites the compiler points at, register the UI form. No reflection, no registry, no runtime dispatch.
### Per-type cores
@@ -296,14 +296,14 @@ pub enum SymbolCharset {
}
```
Single canonical generator implementation in `idfoto-core`, exposed via WASM and used by CLI directly. Both paths use `getrandom`-backed `OsRng` and `rand::distributions::Uniform` for unbiased sampling.
Single canonical generator implementation in `relicario-core`, exposed via WASM and used by CLI directly. Both paths use `getrandom`-backed `OsRng` and `rand::distributions::Uniform` for unbiased sampling.
## Storage, Manifest & Sync
### Repo layout
```
.idfoto/
.relicario/
salt # 32-byte vault salt (KDF input)
params.json # Argon2id parameters, format version
devices.json # authorized device ed25519 pubkeys
@@ -405,7 +405,7 @@ pub fn derive_master_key(
image_secret: &[u8; 32],
salt: &[u8; 32],
params: &Argon2Params,
) -> Result<Zeroizing<[u8; 32]>, IdfotoError> {
) -> Result<Zeroizing<[u8; 32]>, RelicarioError> {
let passphrase_nfc = passphrase.nfc().collect::<String>(); // normalize once
let mut password = Zeroizing::new(
@@ -461,13 +461,13 @@ Per-encryption layout (item, manifest, settings, each attachment):
- `VERSION_BYTE = 0x02` (clean break — no v1 compat).
- XChaCha20-Poly1305 (already correct, audit confirmed-safe #1).
- Fresh `OsRng`-derived nonce per encryption.
- Decrypt failure returns opaque `IdfotoError::Decrypt` regardless of which validation tripped (audit M4).
- Decrypt failure returns opaque `RelicarioError::Decrypt` regardless of which validation tripped (audit M4).
### RNG (audit H5, H6)
- `idfoto-wasm` uses `getrandom` (with `js` feature) for password generation, item IDs, attachment IDs. **No `Math.random()` anywhere.**
- `relicario-wasm` uses `getrandom` (with `js` feature) for password generation, item IDs, attachment IDs. **No `Math.random()` anywhere.**
- Modulo-bias eliminated via `rand::distributions::Uniform` for charset sampling — both CLI and WASM paths.
- Single canonical `generate_password` and `generate_bip39` in `idfoto-core`, exposed to WASM and called directly by CLI.
- Single canonical `generate_password` and `generate_bip39` in `relicario-core`, exposed to WASM and called directly by CLI.
### ID format (audit M8)
@@ -476,14 +476,14 @@ Per-encryption layout (item, manifest, settings, each attachment):
### Per-vault crypto metadata
`.idfoto/params.json`:
`.relicario/params.json`:
```json
{
"format_version": 2,
"kdf": { "algorithm": "argon2id-v0x13", "m": 65536, "t": 3, "p": 4 },
"aead": "xchacha20poly1305",
"salt_path": ".idfoto/salt"
"salt_path": ".relicario/salt"
}
```
@@ -493,7 +493,7 @@ Three version fields exist intentionally and evolve independently:
| Field | Where | Bumps when |
|---|---|---|
| `format_version` | `.idfoto/params.json` | Overall vault layout changes (file structure, KDF construction, anything cross-cutting) |
| `format_version` | `.relicario/params.json` | Overall vault layout changes (file structure, KDF construction, anything cross-cutting) |
| `schema_version` | inside `manifest.enc` | Manifest entry shape changes only (e.g., adding a new field to `ManifestEntry`) |
| `VERSION_BYTE` | first byte of every AEAD blob | AEAD construction itself changes (cipher, nonce size, tag layout) |
@@ -505,7 +505,7 @@ All three set to `2` for the initial typed-item release. Future bumps are indepe
- `setup.html` and `setup.js` **removed from `web_accessible_resources`** in both `extension/manifest.json` and `extension/manifest.firefox.json`.
- The popup opens setup via `chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') })` — own-origin extension tabs work without WAR.
- WASM artifacts (`idfoto_wasm.js`, `idfoto_wasm_bg.wasm`) removed from WAR — service worker loads them via `import` from extension origin.
- WASM artifacts (`relicario_wasm.js`, `relicario_wasm_bg.wasm`) removed from WAR — service worker loads them via `import` from extension origin.
### Split message router (audit C1, C2, C4)
@@ -593,7 +593,7 @@ root.appendChild(promptDom);
Strict rules for content-script DOM construction:
1. **No `innerHTML` anywhere in content scripts.** All construction via `document.createElement` + `.textContent =`.
2. **Element IDs randomized per-prompt** (no stable `idfoto-save-btn` for page collisions). Use a per-prompt `Map<string, HTMLElement>` to wire up handlers.
2. **Element IDs randomized per-prompt** (no stable `relicario-save-btn` for page collisions). Use a per-prompt `Map<string, HTMLElement>` to wire up handlers.
3. **Page-derived values bounded** — username field from `findUsernameValue` capped at 256 chars, control characters stripped, then assigned only via `.textContent`.
4. **CSS scoped via Shadow DOM** — no leak to/from page CSS.
@@ -657,7 +657,7 @@ These audit items are bundled into the Phase 0 remediation plan (not this Phase
- M7: CLI stdout `Password: ********` by default + `--show` flag.
- M11: CLI ISO-8601 timestamp formatting.
- L7: `cargo audit` / `cargo deny` CI configuration.
- L8: CLI vault-dir detection (refuse to operate outside an `.idfoto/`-marked directory).
- L8: CLI vault-dir detection (refuse to operate outside an `.relicario/`-marked directory).
## WASM API Surface
@@ -720,38 +720,38 @@ New commands and renamed semantics:
```bash
# Existing (semantics carry forward, terminology updated to "item")
idfoto init
idfoto unlock # unlocks for next command
idfoto lock
idfoto sync # git pull --rebase + push, hardened
idfoto generate [--length N] [--bip39 [--words N]] [--symbols safe|extended]
idfoto device <add|list|revoke>
relicario init
relicario unlock # unlocks for next command
relicario lock
relicario sync # git pull --rebase + push, hardened
relicario generate [--length N] [--bip39 [--words N]] [--symbols safe|extended]
relicario device <add|list|revoke>
# Updated for typed items
idfoto add <type> [--title T] [--group G] [--tags t1,t2] [--favorite]
relicario add <type> [--title T] [--group G] [--tags t1,t2] [--favorite]
[...type-specific fields, e.g., --username, --url, --password-prompt]
idfoto get <id-or-title> # always concealed by default; --show to reveal
idfoto list [--type T] [--group G] [--tag T] [--trashed]
idfoto edit <id-or-title> # interactive prompts for fields to update
relicario get <id-or-title> # always concealed by default; --show to reveal
relicario list [--type T] [--group G] [--tag T] [--trashed]
relicario edit <id-or-title> # interactive prompts for fields to update
# (no $EDITOR with plaintext — temp-file leak risk)
idfoto rm <id-or-title> # soft-delete (trash)
idfoto restore <id-or-title> # restore from trash
idfoto purge <id-or-title> # hard-delete (also purges attachments)
idfoto trash empty # hard-delete all past retention
relicario rm <id-or-title> # soft-delete (trash)
relicario restore <id-or-title> # restore from trash
relicario purge <id-or-title> # hard-delete (also purges attachments)
relicario trash empty # hard-delete all past retention
# New for attachments
idfoto attach <id-or-title> <file> # adds file as attachment
idfoto attachments <id-or-title> # list attachments on item
idfoto extract <id-or-title> <aid> [--out path] # decrypt + save to disk
relicario attach <id-or-title> <file> # adds file as attachment
relicario attachments <id-or-title> # list attachments on item
relicario extract <id-or-title> <aid> [--out path] # decrypt + save to disk
# Settings
idfoto settings get [<key>]
idfoto settings set <key> <value> # e.g., trash_retention=days:60
relicario settings get [<key>]
relicario settings set <key> <value> # e.g., trash_retention=days:60
```
`idfoto get` shows password as `********` by default. `--show` is required to print plaintext. Clipboard auto-clear unconditional after 30s with `Zeroizing<String>` wrap (audit M6, M7).
`relicario get` shows password as `********` by default. `--show` is required to print plaintext. Clipboard auto-clear unconditional after 30s with `Zeroizing<String>` wrap (audit M6, M7).
`vault_dir()` detection: traverses up from CWD looking for `.idfoto/`. Refuses to operate without one (audit L8).
`vault_dir()` detection: traverses up from CWD looking for `.relicario/`. Refuses to operate without one (audit L8).
## Browser Extension UI Implications
@@ -792,7 +792,7 @@ Setup wizard, capture flow, autofill icon, and unlock screen all continue to exi
- **Service worker router**: mock `chrome.runtime.onMessage` and verify each message type is rejected when sent from the wrong sender (popup-only from content, content-callable from popup, anything from external).
- **Origin-bound autofill**: mock `sender.tab.url` and verify cross-origin requests are rejected even when the content script asks nicely.
- **Closed Shadow DOM**: render the capture prompt, verify the page-side `document.querySelector('#idfoto-save-btn')` returns null.
- **Closed Shadow DOM**: render the capture prompt, verify the page-side `document.querySelector('#relicario-save-btn')` returns null.
- **Generator**: verify no `Math.random()` reachable from any extension entry point (lint rule + runtime probe).
### Manual / observational
@@ -826,7 +826,7 @@ Setup wizard, capture flow, autofill icon, and unlock screen all continue to exi
| H6 | High | `rand::distributions::Uniform` in CLI generator |
| H7 | High | Bump `rpassword` to 7.x (Phase 0) |
| H8 | High | Documented in setup wizard + README; fine-grained PAT guidance |
| M4 | Medium | Opaque `IdfotoError::Decrypt` for all decrypt failures |
| M4 | Medium | Opaque `RelicarioError::Decrypt` for all decrypt failures |
| M5 | Medium | Popup captures `(tab.id, tab.url)` at open; verifies on `fill_credentials` |
| M8 | Medium | 16-char hex IDs |
| M9 | Medium | Item type discriminant validation in deserializer |
@@ -839,20 +839,20 @@ Phase 0 implementation handles the remaining items (M3, M6, M7, M11, L7, L8) out
New files (Rust):
```
crates/idfoto-core/src/item.rs # Item, Section, Field, FieldKind, FieldValue
crates/idfoto-core/src/item_types/mod.rs
crates/idfoto-core/src/item_types/login.rs
crates/idfoto-core/src/item_types/secure_note.rs
crates/idfoto-core/src/item_types/identity.rs
crates/idfoto-core/src/item_types/card.rs
crates/idfoto-core/src/item_types/key.rs
crates/idfoto-core/src/item_types/document.rs
crates/idfoto-core/src/item_types/totp.rs
crates/idfoto-core/src/manifest.rs # rewritten
crates/idfoto-core/src/settings.rs
crates/idfoto-core/src/generators.rs
crates/idfoto-core/src/attachment.rs
crates/idfoto-wasm/src/session.rs
crates/relicario-core/src/item.rs # Item, Section, Field, FieldKind, FieldValue
crates/relicario-core/src/item_types/mod.rs
crates/relicario-core/src/item_types/login.rs
crates/relicario-core/src/item_types/secure_note.rs
crates/relicario-core/src/item_types/identity.rs
crates/relicario-core/src/item_types/card.rs
crates/relicario-core/src/item_types/key.rs
crates/relicario-core/src/item_types/document.rs
crates/relicario-core/src/item_types/totp.rs
crates/relicario-core/src/manifest.rs # rewritten
crates/relicario-core/src/settings.rs
crates/relicario-core/src/generators.rs
crates/relicario-core/src/attachment.rs
crates/relicario-wasm/src/session.rs
```
New files (extension):
@@ -875,15 +875,15 @@ extension/src/popup/components/history.ts
Heavily modified (Rust):
```
crates/idfoto-core/src/lib.rs # re-exports + module declarations
crates/idfoto-core/src/crypto.rs # length-prefix KDF, Zeroize, NFC
crates/idfoto-core/src/entry.rs # DELETED — replaced by item.rs
crates/idfoto-core/src/error.rs # opaque Decrypt variant only
crates/idfoto-core/Cargo.toml # add zeroize, zxcvbn, bip39, unicode-normalization
crates/idfoto-wasm/src/lib.rs # session-handle API, getrandom
crates/idfoto-wasm/Cargo.toml # update deps
crates/idfoto-cli/src/main.rs # rewritten command handlers
crates/idfoto-cli/Cargo.toml # rpassword = "7", clipboard hardening
crates/relicario-core/src/lib.rs # re-exports + module declarations
crates/relicario-core/src/crypto.rs # length-prefix KDF, Zeroize, NFC
crates/relicario-core/src/entry.rs # DELETED — replaced by item.rs
crates/relicario-core/src/error.rs # opaque Decrypt variant only
crates/relicario-core/Cargo.toml # add zeroize, zxcvbn, bip39, unicode-normalization
crates/relicario-wasm/src/lib.rs # session-handle API, getrandom
crates/relicario-wasm/Cargo.toml # update deps
crates/relicario-cli/src/main.rs # rewritten command handlers
crates/relicario-cli/Cargo.toml # rpassword = "7", clipboard hardening
```
Heavily modified (extension):
@@ -916,5 +916,5 @@ Documentation:
```
README.md # update for typed items, security warnings
CLAUDE.md # reflect new module structure
docs/superpowers/specs/2026-04-11-idfoto-design.md # amend KDF section per H1; note format v2
docs/superpowers/specs/2026-04-11-relicario-design.md # amend KDF section per H1; note format v2
```