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:
@@ -1,7 +1,7 @@
|
||||
# idfoto Security Audit Report
|
||||
# relicario Security Audit Report
|
||||
|
||||
**Date:** 2026-04-18
|
||||
**Scope:** Full static review of `crates/idfoto-core/`, `crates/idfoto-cli/`, `crates/idfoto-wasm/`, `extension/src/`, both manifests, both webpack configs, and the design spec at `docs/superpowers/specs/2026-04-11-idfoto-design.md`.
|
||||
**Scope:** Full static review of `crates/relicario-core/`, `crates/relicario-cli/`, `crates/relicario-wasm/`, `extension/src/`, both manifests, both webpack configs, and the design spec at `docs/superpowers/specs/2026-04-11-relicario-design.md`.
|
||||
**Methodology:** Static review against the project's documented threat model.
|
||||
|
||||
---
|
||||
@@ -26,7 +26,7 @@ This breaks the second of the four security invariants in the design spec ("Two-
|
||||
|
||||
**Remediation:**
|
||||
|
||||
1. Remove `setup.html`, `setup.js`, `idfoto_wasm.js`, and `idfoto_wasm_bg.wasm` from `web_accessible_resources` entirely. The setup page is opened with `chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') })` from the popup (`setup-wizard.ts:28`), which works fine without `web_accessible_resources` for own-origin tabs.
|
||||
1. Remove `setup.html`, `setup.js`, `relicario_wasm.js`, and `relicario_wasm_bg.wasm` from `web_accessible_resources` entirely. The setup page is opened with `chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') })` from the popup (`setup-wizard.ts:28`), which works fine without `web_accessible_resources` for own-origin tabs.
|
||||
2. In the `save_setup` handler, validate the sender: require `sender.id === chrome.runtime.id` AND `sender.url?.startsWith(chrome.runtime.getURL('setup.html'))`. Reject all other senders.
|
||||
3. If a vault is already configured, require an explicit user confirmation in the popup before overwriting — don't silently swap the binding.
|
||||
4. Consider hashing the (config, imageBase64) tuple and surfacing a fingerprint to the user so a swap is at least visible.
|
||||
@@ -63,12 +63,12 @@ a) `escapeForHtml` uses the `div.textContent` round-trip trick. That escapes `&`
|
||||
|
||||
The textContent round-trip *does* escape `<`, `>`, and `&`, so injection of raw `<img>` tags is blocked. But:
|
||||
|
||||
b) The DOM the script is constructing lives in the **page's** document, not the extension's. Even if the escape were perfect, the page's existing CSS/JS sees the prompt and can read its DOM (`#idfoto-capture-prompt`, `#idfoto-save-btn`, etc.). Page JS can:
|
||||
b) The DOM the script is constructing lives in the **page's** document, not the extension's. Even if the escape were perfect, the page's existing CSS/JS sees the prompt and can read its DOM (`#relicario-capture-prompt`, `#relicario-save-btn`, etc.). Page JS can:
|
||||
- Wait for the prompt to appear via `MutationObserver`, read the `<strong>` text to learn the username being saved.
|
||||
- Programmatically `.click()` `#idfoto-save-btn` to silently save attacker-substituted credentials to the user's vault. (The `Save` handler reads `username` and `password` from variables captured at `showPrompt` call time, so it'll save *correct* values — but the page can replace the button's click listener via `cloneNode`/`replaceWith` or wrap it.)
|
||||
- Programmatically `.click()` `#idfoto-never-btn` to suppress capture for the user's *real* sites by getting them blacklisted via a confusable hostname.
|
||||
- Programmatically `.click()` `#relicario-save-btn` to silently save attacker-substituted credentials to the user's vault. (The `Save` handler reads `username` and `password` from variables captured at `showPrompt` call time, so it'll save *correct* values — but the page can replace the button's click listener via `cloneNode`/`replaceWith` or wrap it.)
|
||||
- Programmatically `.click()` `#relicario-never-btn` to suppress capture for the user's *real* sites by getting them blacklisted via a confusable hostname.
|
||||
|
||||
c) The injected button uses `id="idfoto-save-btn"`. If the page has its own element with the same id, document.getElementById on subsequent saves returns whichever the browser returns first — generally the page's. Use a Shadow DOM or unique random ids per-prompt instead.
|
||||
c) The injected button uses `id="relicario-save-btn"`. If the page has its own element with the same id, document.getElementById on subsequent saves returns whichever the browser returns first — generally the page's. Use a Shadow DOM or unique random ids per-prompt instead.
|
||||
|
||||
**Why it matters:** The capture flow is the easiest path to silent credential exfiltration. A malicious site can craft inputs and DOM such that submitting *any* form on the page causes the user's vault to capture and save attacker-chosen credentials labeled as the user's bank/email, or such that legitimate save prompts get `Never`-clicked and silently blacklisted.
|
||||
|
||||
@@ -77,7 +77,7 @@ c) The injected button uses `id="idfoto-save-btn"`. If the page has its own elem
|
||||
1. Render the prompt inside a closed Shadow DOM: `const root = container.attachShadow({ mode: 'closed' });` then `root.innerHTML = ...`. Closed shadow DOM is invisible to the page's JS.
|
||||
2. Replace `escapeForHtml(displayUser)` with `textContent` assignments rather than `innerHTML`. Construct the DOM with `document.createElement` + `.textContent =` for any attacker-derived strings.
|
||||
3. Treat all values from `findUsernameValue` as fully untrusted; sanity-check they're not control characters or exceptionally long.
|
||||
4. Do not use stable IDs (`idfoto-save-btn`) on elements injected into a hostile DOM.
|
||||
4. Do not use stable IDs (`relicario-save-btn`) on elements injected into a hostile DOM.
|
||||
|
||||
---
|
||||
|
||||
@@ -93,7 +93,7 @@ c) The injected button uses `id="idfoto-save-btn"`. If the page has its own elem
|
||||
|
||||
The icon-click flow is presented as the "intended" path, but nothing in the code enforces that the icon must be the trigger. The design spec section "Autofill anti-phishing (origin checks)" is referenced in the audit prompt but is not implemented anywhere.
|
||||
|
||||
**Why it matters:** This is the classic phishing primitive a password manager exists to prevent. idfoto currently has weaker origin discipline than even a manually-typed-in form would have.
|
||||
**Why it matters:** This is the classic phishing primitive a password manager exists to prevent. relicario currently has weaker origin discipline than even a manually-typed-in form would have.
|
||||
|
||||
**Remediation:**
|
||||
|
||||
@@ -108,7 +108,7 @@ The icon-click flow is presented as the "intended" path, but nothing in the code
|
||||
|
||||
### H1. Argon2id password input is the unprefixed concatenation of passphrase || image_secret — collision-engineerable second-preimage path
|
||||
|
||||
**File:** `crates/idfoto-core/src/crypto.rs:225-227`.
|
||||
**File:** `crates/relicario-core/src/crypto.rs:225-227`.
|
||||
|
||||
**Issue:** `password = passphrase || image_secret`. Two distinct (passphrase, image_secret) pairs produce the same Argon2id input — e.g. `("abc", [0x44, 0x55, …])` and `("abcD", [0x55, …])` differ only in where the boundary sits but produce identical concatenations and therefore identical master keys. The design spec explicitly calls this out as "the canonical Argon2id API — no custom construction" but it's not canonical at all; concatenating two variable-length values without a length prefix is a textbook construction smell.
|
||||
|
||||
@@ -131,9 +131,9 @@ Cite spec line: the spec at "Key derivation" explicitly says "concatenated, 32-b
|
||||
|
||||
### H2. Master key never zeroized; `Vec<u8>` from `derive_master_key` and intermediate buffers leak into reallocated heap
|
||||
|
||||
**File:** `crates/idfoto-core/src/crypto.rs:205-235`, `crates/idfoto-cli/src/main.rs:204-218` and every command that calls `unlock`.
|
||||
**File:** `crates/relicario-core/src/crypto.rs:205-235`, `crates/relicario-cli/src/main.rs:204-218` and every command that calls `unlock`.
|
||||
|
||||
**Issue:** The Argon2id output (`output: [u8; 32]`) is returned by value, copied into an owned `Vec` in `idfoto-wasm`'s `derive_master_key` (`lib.rs:62`), then handed to JS as a `Uint8Array` whose backing memory lives in the WASM linear memory. Nothing implements `Drop` to wipe the bytes. The intermediate `password` Vec at `crypto.rs:225-227` (which contains the *passphrase plaintext* alongside the image_secret) is also dropped without zeroizing — its buffer is freed and may be reallocated for unrelated purposes, retaining the passphrase in process memory until overwritten.
|
||||
**Issue:** The Argon2id output (`output: [u8; 32]`) is returned by value, copied into an owned `Vec` in `relicario-wasm`'s `derive_master_key` (`lib.rs:62`), then handed to JS as a `Uint8Array` whose backing memory lives in the WASM linear memory. Nothing implements `Drop` to wipe the bytes. The intermediate `password` Vec at `crypto.rs:225-227` (which contains the *passphrase plaintext* alongside the image_secret) is also dropped without zeroizing — its buffer is freed and may be reallocated for unrelated purposes, retaining the passphrase in process memory until overwritten.
|
||||
|
||||
In the CLI, the passphrase string from `rpassword::prompt_password_stderr` (an owned `String`) is also not zeroized. The `master_key: [u8; 32]` returned from `unlock` is just a stack array — better — but it gets passed by reference to `encrypt_entry` etc. which call into XChaCha20Poly1305 internals that may copy the key.
|
||||
|
||||
@@ -141,7 +141,7 @@ In the CLI, the passphrase string from `rpassword::prompt_password_stderr` (an o
|
||||
|
||||
**Remediation:**
|
||||
|
||||
1. Add `zeroize = "1"` and `zeroize_derive` to `idfoto-core`.
|
||||
1. Add `zeroize = "1"` and `zeroize_derive` to `relicario-core`.
|
||||
2. Wrap `master_key` in `Zeroizing<[u8; 32]>` in both `derive_master_key` return and at all CLI/WASM call sites.
|
||||
3. Wrap the temporary `password` Vec in `Zeroizing<Vec<u8>>` so its contents are wiped on drop.
|
||||
4. In the CLI, zeroize the passphrase string immediately after passing into `derive_master_key`.
|
||||
@@ -152,7 +152,7 @@ In the CLI, the passphrase string from `rpassword::prompt_password_stderr` (an o
|
||||
|
||||
### H3. Passphrase strength gate is purely cosmetic; the only enforced minimum is 8 characters
|
||||
|
||||
**File:** `crates/idfoto-cli/src/main.rs:354-356`, `extension/src/setup/setup.ts:74-85, 363-373`.
|
||||
**File:** `crates/relicario-cli/src/main.rs:354-356`, `extension/src/setup/setup.ts:74-85, 363-373`.
|
||||
|
||||
**Issue:** The CLI requires `>= 8` characters — no entropy enforcement. The extension calls `passphraseStrength()` purely for the colored bar; the create-vault step accepts any non-empty passphrase including a single character (`if (!state.passphrase) bail`). This contradicts the spec's "Adversaries → Stolen device + weak passphrase: enforce minimum passphrase strength at vault creation" defense.
|
||||
|
||||
@@ -170,7 +170,7 @@ The threat model says the passphrase carries the entire entropy load against an
|
||||
|
||||
### H4. CLI git_commit shells out without disabling pager / signed commits / hooks; no git config isolation
|
||||
|
||||
**File:** `crates/idfoto-cli/src/main.rs:239-257, 402-405, 736-756`.
|
||||
**File:** `crates/relicario-cli/src/main.rs:239-257, 402-405, 736-756`.
|
||||
|
||||
**Issue:** Every CLI mutation runs `git add -A` then `git commit -m <message>`. There are no environmental guards:
|
||||
|
||||
@@ -189,13 +189,13 @@ Command::new("git")
|
||||
"-c", "core.editor=true", "commit", "-m", message])
|
||||
```
|
||||
|
||||
Stage only the specific files the operation touched (`entries/<id>.enc`, `manifest.enc`, `.idfoto/devices.json`) instead of `git add -A`.
|
||||
Stage only the specific files the operation touched (`entries/<id>.enc`, `manifest.enc`, `.relicario/devices.json`) instead of `git add -A`.
|
||||
|
||||
---
|
||||
|
||||
### H5. WASM `generate_password` uses `Math.random()` — claimed "non-security-critical" is wrong
|
||||
|
||||
**File:** `crates/idfoto-wasm/src/lib.rs:240-256`.
|
||||
**File:** `crates/relicario-wasm/src/lib.rs:240-256`.
|
||||
|
||||
**Issue:** The doc comment says "Uses `js_sys::Math::random()` for randomness (not cryptographically secure, but sufficient for password character selection)." This is **flatly wrong**. Generated passwords are the user's stored credential for whatever site they're saving — they must be CSPRNG-derived. `Math.random()` is V8's xorshift128+ which is:
|
||||
|
||||
@@ -205,7 +205,7 @@ Stage only the specific files the operation touched (`entries/<id>.enc`, `manife
|
||||
|
||||
The ext-bundled `crypto.getRandomValues` is available in service-worker context (it's used at `setup.ts:384`). There is no reason to use `Math.random` here.
|
||||
|
||||
**Remediation:** Replace both `generate_password` and `generate_entry_id` in `idfoto-wasm` to use `getrandom` (already in the dependency list with `features = ["js"]` enabled, line in `Cargo.toml`). Equivalent to:
|
||||
**Remediation:** Replace both `generate_password` and `generate_entry_id` in `relicario-wasm` to use `getrandom` (already in the dependency list with `features = ["js"]` enabled, line in `Cargo.toml`). Equivalent to:
|
||||
|
||||
```rust
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
@@ -221,7 +221,7 @@ Also: the modulo-by-charset-length introduces small bias (`CHARSET.len() = 87`,
|
||||
|
||||
### H6. CLI password generator has modulo bias
|
||||
|
||||
**File:** `crates/idfoto-cli/src/main.rs:308-317`.
|
||||
**File:** `crates/relicario-cli/src/main.rs:308-317`.
|
||||
|
||||
**Issue:** `(rng.next_u32() as usize) % CHARSET.len()` where `CHARSET.len() == 75`. Since `2^32 % 75 = 1` (≈), bias is mild, but still nonzero. For a tool whose entire job is generating high-entropy secrets, use `rand::distributions::Uniform` or rejection sampling.
|
||||
|
||||
@@ -236,7 +236,7 @@ let dist = Uniform::from(0..CHARSET.len());
|
||||
|
||||
### H7. `rpassword 5.0.1` is from 2020 and the API used (`prompt_password_stderr`) was deprecated and removed in 6.x
|
||||
|
||||
**File:** `crates/idfoto-cli/Cargo.toml` (`rpassword = "5"`), `main.rs:205, 352, 358`.
|
||||
**File:** `crates/relicario-cli/Cargo.toml` (`rpassword = "5"`), `main.rs:205, 352, 358`.
|
||||
|
||||
**Issue:** `rpassword 5.0.1` predates several documented platform handling fixes (Windows console, terminal-restoration on signal). The current crate is at 7.x. `prompt_password_stderr` was removed; use `prompt_password` and pipe it to stderr separately, or call `rpassword::prompt_password_from_bufread` for testability. Stale dep is a supply-chain hygiene issue and may carry unfixed terminal-restoration bugs that leave the TTY in no-echo mode if the user Ctrl-C's mid-prompt.
|
||||
|
||||
@@ -266,19 +266,19 @@ The spec says this is "acceptable" and that the reference image is supposed to l
|
||||
|
||||
### M1. `read_block` panics on out-of-bounds via `read_block_abs(...).unwrap()`
|
||||
|
||||
`crates/idfoto-core/src/imgsecret.rs:252-256`. Future block-selection changes could panic at runtime; in WASM this aborts the whole service worker. Return `Result` and propagate, or `debug_assert!`.
|
||||
`crates/relicario-core/src/imgsecret.rs:252-256`. Future block-selection changes could panic at runtime; in WASM this aborts the whole service worker. Return `Result` and propagate, or `debug_assert!`.
|
||||
|
||||
### M2. `bits_to_bytes` length not validated in `try_extract_with_layout`
|
||||
|
||||
`crates/idfoto-core/src/imgsecret.rs:765-768`. `secret.copy_from_slice(&result_bytes[..32])` panics if `result_bytes.len() < 32`. Add `debug_assert_eq!` and prefer `try_into()`.
|
||||
`crates/relicario-core/src/imgsecret.rs:765-768`. `secret.copy_from_slice(&result_bytes[..32])` panics if `result_bytes.len() < 32`. Add `debug_assert_eq!` and prefer `try_into()`.
|
||||
|
||||
### M3. `extract_with_crop_recovery` has unbounded compute for attacker-controlled JPEG dimensions
|
||||
|
||||
`crates/idfoto-core/src/imgsecret.rs:784-833`. A 32000×32000 attacker-supplied JPEG can wedge the service worker for tens of seconds. Cap `MAX_DIMENSION` (e.g. 10000 px) and peek dimensions before full decode.
|
||||
`crates/relicario-core/src/imgsecret.rs:784-833`. A 32000×32000 attacker-supplied JPEG can wedge the service worker for tens of seconds. Cap `MAX_DIMENSION` (e.g. 10000 px) and peek dimensions before full decode.
|
||||
|
||||
### M4. `decrypt` error path leaks coarse timing about which validation failed first
|
||||
|
||||
`crates/idfoto-core/src/crypto.rs:115-141`. Not exploitable today (only attacker-supplied ciphertexts are the user's own files). If a "share an entry" feature lands, this becomes a side channel. Consider returning `IdfotoError::Decrypt` for all failure modes.
|
||||
`crates/relicario-core/src/crypto.rs:115-141`. Not exploitable today (only attacker-supplied ciphertexts are the user's own files). If a "share an entry" feature lands, this becomes a side channel. Consider returning `RelicarioError::Decrypt` for all failure modes.
|
||||
|
||||
### M5. `chrome.tabs.sendMessage` in fill_credentials sends to currently-active tab without verifying the tab matches the entry's origin
|
||||
|
||||
@@ -286,19 +286,19 @@ The spec says this is "acceptable" and that the reference image is supposed to l
|
||||
|
||||
### M6. CLI clipboard clear is best-effort and racy
|
||||
|
||||
`crates/idfoto-cli/src/main.rs:565-585`. The 30s clear thread holds a *clone* of the plaintext password for 30 seconds and won't clear if user copies anything else and back. Always clear unconditionally; wrap in `Zeroizing<String>`.
|
||||
`crates/relicario-cli/src/main.rs:565-585`. The 30s clear thread holds a *clone* of the plaintext password for 30 seconds and won't clear if user copies anything else and back. Always clear unconditionally; wrap in `Zeroizing<String>`.
|
||||
|
||||
### M7. CLI prints the full password to stdout via `println!`
|
||||
|
||||
`crates/idfoto-cli/src/main.rs:553`. `idfoto get` prints `"Password: <plaintext>"` to stdout — ends up in scrollback, `script` transcripts, tmux capture, pipes. Show `********` by default; require `--show` flag.
|
||||
`crates/relicario-cli/src/main.rs:553`. `relicario get` prints `"Password: <plaintext>"` to stdout — ends up in scrollback, `script` transcripts, tmux capture, pipes. Show `********` by default; require `--show` flag.
|
||||
|
||||
### M8. CLI generates entry IDs with only 32 bits of randomness; 8-char hex collisions are realistic
|
||||
|
||||
`crates/idfoto-core/src/entry.rs:159-163`. Birthday-bound: ~65k entries gives ~50% collision; `manifest.add_entry` silently overwrites. Bump to 16-char hex (64 bits), or check before write.
|
||||
`crates/relicario-core/src/entry.rs:159-163`. Birthday-bound: ~65k entries gives ~50% collision; `manifest.add_entry` silently overwrites. Bump to 16-char hex (64 bits), or check before write.
|
||||
|
||||
### M9. WASM TOTP code has no guard against `result[offset + 3]` index when HMAC output is exactly 20 bytes
|
||||
|
||||
`crates/idfoto-wasm/src/lib.rs:227-232`. Safe today (HMAC-SHA1 is always 20 bytes, max offset is 15). Add `debug_assert_eq!(result.len(), 20)` for future-proofing.
|
||||
`crates/relicario-wasm/src/lib.rs:227-232`. Safe today (HMAC-SHA1 is always 20 bytes, max offset is 15). Add `debug_assert_eq!(result.len(), 20)` for future-proofing.
|
||||
|
||||
### M10. `setup-wizard.ts` opens a new tab, but `window.close()` is no-op if popup is not in popup context
|
||||
|
||||
@@ -306,24 +306,24 @@ The spec says this is "acceptable" and that the reference image is supposed to l
|
||||
|
||||
### M11. CLI `now_iso8601` returns Unix seconds but the field is named `iso8601` and the spec promises ISO 8601 formatting
|
||||
|
||||
`crates/idfoto-cli/src/main.rs:263-268`. Function name lies; consumers may parse timestamps and silently mishandle a numeric value. Either rename or use chrono/jiff.
|
||||
`crates/relicario-cli/src/main.rs:263-268`. Function name lies; consumers may parse timestamps and silently mishandle a numeric value. Either rename or use chrono/jiff.
|
||||
|
||||
### M12. `arboard 3` carries platform-dependent behavior; password may persist after `set_text("")` on Linux X11
|
||||
|
||||
`crates/idfoto-cli/src/main.rs:572-579`. Document Linux limitations.
|
||||
`crates/relicario-cli/src/main.rs:572-579`. Document Linux limitations.
|
||||
|
||||
---
|
||||
|
||||
## LOW / INFORMATIONAL
|
||||
|
||||
- **L1.** Dead-code-allowed fields in `EmbedRegion` (`crates/idfoto-core/src/imgsecret.rs:163, 166`).
|
||||
- **L2.** `IdfotoError::Format` exposes the offending version byte in user-facing error string. Minor info disclosure.
|
||||
- **L1.** Dead-code-allowed fields in `EmbedRegion` (`crates/relicario-core/src/imgsecret.rs:163, 166`).
|
||||
- **L2.** `RelicarioError::Format` exposes the offending version byte in user-facing error string. Minor info disclosure.
|
||||
- **L3.** Capture flow's `check_credential` decrypts every candidate entry on every form submit (`index.ts:421-423`). Cache password hash, not password.
|
||||
- **L4.** `popup.ts:16-20` `setState` triggers full re-render every state change — in-flight async responses can race and double-fire.
|
||||
- **L5.** Chrome MV3 manifest CSP includes `'wasm-unsafe-eval'` — required but document why.
|
||||
- **L6.** `git-host.ts:27` uses `String.fromCharCode(bytes[i])` for base64 — vulnerable to memory pressure with large reference images. Use chunked or `FileReader`.
|
||||
- **L7.** `Cargo.toml` allows wide major-version ranges. No `cargo audit` / `cargo deny` config in repo.
|
||||
- **L8.** CLI `vault_dir()` silently returns `current_dir()` — `idfoto add` in `/home` will start writing files there. Detect missing `.idfoto/` and bail.
|
||||
- **L8.** CLI `vault_dir()` silently returns `current_dir()` — `relicario add` in `/home` will start writing files there. Detect missing `.relicario/` and bail.
|
||||
- **L9.** `devices.json` initial write differs between CLI (`"[]"`) and extension (`'{"devices":[]}'`). Schema mismatch.
|
||||
- **L10.** `totpSecretCache` (`Map<string, string>` of plaintext base32 secrets) has no zeroization — note that JS strings can't be zeroized.
|
||||
- **L11.** `escapeHtml` at `popup.ts:16-20` doesn't escape `'` (single quote). Codebase uses double quotes for attributes, so currently safe but fragile.
|
||||
@@ -341,7 +341,7 @@ These primitives and parameters are correctly used and do **not** need further w
|
||||
4. **`crypto.getRandomValues`** in setup wizard for image_secret + salt (`setup.ts:383-393`).
|
||||
5. **ed25519-dalek 2.2.0** with `rand_core` — modern strict-verification version.
|
||||
6. **TOTP / RFC 6238** in WASM is correct; unit tests exercise published RFC test vectors (`wasm/lib.rs:280-301`).
|
||||
7. **AEAD failure → opaque `IdfotoError::Decrypt`** with generic message ("wrong key or corrupted data"). Avoids leaking which factor is wrong (`error.rs:33`, `crypto.rs:138`).
|
||||
7. **AEAD failure → opaque `RelicarioError::Decrypt`** with generic message ("wrong key or corrupted data"). Avoids leaking which factor is wrong (`error.rs:33`, `crypto.rs:138`).
|
||||
8. **Version byte (0x01)** at start of every ciphertext blob with rejection of unknown versions.
|
||||
9. **Two-factor independence** verified by `tests/integration.rs:120-153`.
|
||||
10. **DCT round-trip correctness** verified to 1e-6 tolerance.
|
||||
@@ -367,6 +367,6 @@ These primitives and parameters are correctly used and do **not** need further w
|
||||
|
||||
## Summary
|
||||
|
||||
idfoto's *core cryptography* is solid: correct AEAD, correct KDF parameters, real two-factor key derivation. The bugs are concentrated in the *extension boundary* and the *plumbing around the crypto*: the setup wizard is web-accessible without sender checks (C1), the message router trusts every caller (C2), capture and autofill have no origin discipline (C3, C4), the WASM password generator is non-cryptographic (H5), and master-key/passphrase memory hygiene is absent (H2).
|
||||
relicario's *core cryptography* is solid: correct AEAD, correct KDF parameters, real two-factor key derivation. The bugs are concentrated in the *extension boundary* and the *plumbing around the crypto*: the setup wizard is web-accessible without sender checks (C1), the message router trusts every caller (C2), capture and autofill have no origin discipline (C3, C4), the WASM password generator is non-cryptographic (H5), and master-key/passphrase memory hygiene is absent (H2).
|
||||
|
||||
**C1–C4 together are exploitable end-to-end and should be treated as release blockers.** H1–H8 should land before any tagged 1.0; M-class items can be batched into hardening PRs.
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# idfoto Core + CLI Implementation Plan
|
||||
# relicario Core + CLI Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a working git-backed password manager with a Rust core library and CLI that can create vaults, add/get/list/edit/rm credentials, sync via git, and manage device keys — all backed by the reference-image + passphrase two-factor KDF.
|
||||
|
||||
**Architecture:** Cargo workspace with two crates: `idfoto-core` (platform-agnostic library — KDF, AEAD, vault format, imgsecret DCT embedding) and `idfoto-cli` (filesystem, git, terminal I/O). The core takes bytes and returns bytes; the CLI handles all platform interaction. TDD throughout.
|
||||
**Architecture:** Cargo workspace with two crates: `relicario-core` (platform-agnostic library — KDF, AEAD, vault format, imgsecret DCT embedding) and `relicario-cli` (filesystem, git, terminal I/O). The core takes bytes and returns bytes; the CLI handles all platform interaction. TDD throughout.
|
||||
|
||||
**Tech Stack:** Rust (stable, 2021 edition), argon2, chacha20poly1305, image, serde/serde_json, clap, ed25519-dalek
|
||||
|
||||
**Scope:** This is Plan 1 of 2. This plan covers `idfoto-core` and `idfoto-cli`. Plan 2 (idfoto-wasm + Chrome extension) follows after this is working. This plan produces a complete, usable CLI password manager.
|
||||
**Scope:** This is Plan 1 of 2. This plan covers `relicario-core` and `relicario-cli`. Plan 2 (relicario-wasm + Chrome extension) follows after this is working. This plan produces a complete, usable CLI password manager.
|
||||
|
||||
**Prerequisites:** Rust stable installed via `rustup`. Git installed. A test JPEG image (any cell phone photo) available for manual testing.
|
||||
|
||||
**Design spec:** `docs/superpowers/specs/2026-04-11-idfoto-design.md`
|
||||
**Design spec:** `docs/superpowers/specs/2026-04-11-relicario-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
idfoto/ (project root = /home/alee/Sources/idfoto)
|
||||
relicario/ (project root = /home/alee/Sources/relicario)
|
||||
├── Cargo.toml # workspace root
|
||||
├── crates/
|
||||
│ ├── idfoto-core/
|
||||
│ ├── relicario-core/
|
||||
│ │ ├── Cargo.toml
|
||||
│ │ └── src/
|
||||
│ │ ├── lib.rs # re-exports public API
|
||||
│ │ ├── error.rs # IdfotoError enum (thiserror)
|
||||
│ │ ├── error.rs # RelicarioError enum (thiserror)
|
||||
│ │ ├── crypto.rs # derive_master_key(), encrypt(), decrypt()
|
||||
│ │ ├── entry.rs # Entry, ManifestEntry, Manifest structs
|
||||
│ │ ├── vault.rs # encrypt/decrypt entries + manifest, binary format
|
||||
│ │ └── imgsecret.rs # embed(), extract() — DCT embedding primitive
|
||||
│ └── idfoto-cli/
|
||||
│ └── relicario-cli/
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
│ └── main.rs # clap CLI with all subcommands
|
||||
├── docs/
|
||||
│ └── superpowers/
|
||||
│ ├── specs/
|
||||
│ │ └── 2026-04-11-idfoto-design.md
|
||||
│ │ └── 2026-04-11-relicario-design.md
|
||||
│ └── plans/
|
||||
│ └── 2026-04-11-idfoto-core-cli.md (this file)
|
||||
│ └── 2026-04-11-relicario-core-cli.md (this file)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
@@ -50,10 +50,10 @@ idfoto/ (project root = /home/alee/Sources/idfoto)
|
||||
|
||||
**Files:**
|
||||
- Create: `Cargo.toml`
|
||||
- Create: `crates/idfoto-core/Cargo.toml`
|
||||
- Create: `crates/idfoto-core/src/lib.rs`
|
||||
- Create: `crates/idfoto-cli/Cargo.toml`
|
||||
- Create: `crates/idfoto-cli/src/main.rs`
|
||||
- Create: `crates/relicario-core/Cargo.toml`
|
||||
- Create: `crates/relicario-core/src/lib.rs`
|
||||
- Create: `crates/relicario-cli/Cargo.toml`
|
||||
- Create: `crates/relicario-cli/src/main.rs`
|
||||
|
||||
- [ ] **Step 1: Create workspace root Cargo.toml**
|
||||
|
||||
@@ -62,20 +62,20 @@ idfoto/ (project root = /home/alee/Sources/idfoto)
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/idfoto-core",
|
||||
"crates/idfoto-cli",
|
||||
"crates/relicario-core",
|
||||
"crates/relicario-cli",
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create idfoto-core crate**
|
||||
- [ ] **Step 2: Create relicario-core crate**
|
||||
|
||||
```toml
|
||||
# crates/idfoto-core/Cargo.toml
|
||||
# crates/relicario-core/Cargo.toml
|
||||
[package]
|
||||
name = "idfoto-core"
|
||||
name = "relicario-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Core library for idfoto password manager"
|
||||
description = "Core library for relicario password manager"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "2"
|
||||
@@ -92,26 +92,26 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
||||
```
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/lib.rs
|
||||
// crates/relicario-core/src/lib.rs
|
||||
pub mod error;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create idfoto-cli crate**
|
||||
- [ ] **Step 3: Create relicario-cli crate**
|
||||
|
||||
```toml
|
||||
# crates/idfoto-cli/Cargo.toml
|
||||
# crates/relicario-cli/Cargo.toml
|
||||
[package]
|
||||
name = "idfoto-cli"
|
||||
name = "relicario-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "CLI for idfoto password manager"
|
||||
description = "CLI for relicario password manager"
|
||||
|
||||
[[bin]]
|
||||
name = "idfoto"
|
||||
name = "relicario"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
idfoto-core = { path = "../idfoto-core" }
|
||||
relicario-core = { path = "../relicario-core" }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
rpassword = "5"
|
||||
@@ -120,9 +120,9 @@ dirs = "5"
|
||||
```
|
||||
|
||||
```rust
|
||||
// crates/idfoto-cli/src/main.rs
|
||||
// crates/relicario-cli/src/main.rs
|
||||
fn main() {
|
||||
println!("idfoto v0.1.0");
|
||||
println!("relicario v0.1.0");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -138,7 +138,7 @@ git init
|
||||
echo "target/" > .gitignore
|
||||
echo ".superpowers/" >> .gitignore
|
||||
git add Cargo.toml crates/ .gitignore docs/
|
||||
git commit -m "feat: scaffold Cargo workspace with idfoto-core and idfoto-cli"
|
||||
git commit -m "feat: scaffold Cargo workspace with relicario-core and relicario-cli"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -146,17 +146,17 @@ git commit -m "feat: scaffold Cargo workspace with idfoto-core and idfoto-cli"
|
||||
### Task 2: Error Types
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/idfoto-core/src/error.rs`
|
||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
||||
- Create: `crates/relicario-core/src/error.rs`
|
||||
- Modify: `crates/relicario-core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write the error enum**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/error.rs
|
||||
// crates/relicario-core/src/error.rs
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IdfotoError {
|
||||
pub enum RelicarioError {
|
||||
#[error("key derivation failed: {0}")]
|
||||
Kdf(String),
|
||||
|
||||
@@ -193,16 +193,16 @@ pub enum IdfotoError {
|
||||
DeviceKey(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, IdfotoError>;
|
||||
pub type Result<T> = std::result::Result<T, RelicarioError>;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update lib.rs to re-export**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/lib.rs
|
||||
// crates/relicario-core/src/lib.rs
|
||||
pub mod error;
|
||||
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify build**
|
||||
@@ -213,8 +213,8 @@ Expected: Compiles cleanly.
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/error.rs crates/idfoto-core/src/lib.rs
|
||||
git commit -m "feat: add IdfotoError enum with thiserror"
|
||||
git add crates/relicario-core/src/error.rs crates/relicario-core/src/lib.rs
|
||||
git commit -m "feat: add RelicarioError enum with thiserror"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -222,13 +222,13 @@ git commit -m "feat: add IdfotoError enum with thiserror"
|
||||
### Task 3: Crypto — Key Derivation
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/idfoto-core/src/crypto.rs`
|
||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
||||
- Create: `crates/relicario-core/src/crypto.rs`
|
||||
- Modify: `crates/relicario-core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/crypto.rs
|
||||
// crates/relicario-core/src/crypto.rs
|
||||
|
||||
// ... (implementation comes in step 3)
|
||||
|
||||
@@ -274,17 +274,17 @@ mod tests {
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p idfoto-core derive_master_key`
|
||||
Run: `cargo test -p relicario-core derive_master_key`
|
||||
Expected: FAIL — `derive_master_key` and `KdfParams` not defined.
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/crypto.rs
|
||||
// crates/relicario-core/src/crypto.rs
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use crate::error::{IdfotoError, Result};
|
||||
use crate::error::{RelicarioError, Result};
|
||||
|
||||
/// Argon2id tuning parameters. Stored in .idfoto/params.json.
|
||||
/// Argon2id tuning parameters. Stored in .relicario/params.json.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KdfParams {
|
||||
/// Memory cost in KiB (default: 65536 = 64 MiB)
|
||||
@@ -308,7 +308,7 @@ impl Default for KdfParams {
|
||||
/// Derive a 32-byte master key from passphrase + image_secret + salt.
|
||||
///
|
||||
/// password = passphrase_bytes || image_secret_bytes (concatenated)
|
||||
/// salt = vault_salt (32 bytes from .idfoto/salt)
|
||||
/// salt = vault_salt (32 bytes from .relicario/salt)
|
||||
pub fn derive_master_key(
|
||||
passphrase: &[u8],
|
||||
image_secret: &[u8; 32],
|
||||
@@ -326,14 +326,14 @@ pub fn derive_master_key(
|
||||
params.argon2_p,
|
||||
Some(32),
|
||||
)
|
||||
.map_err(|e| IdfotoError::Kdf(e.to_string()))?;
|
||||
.map_err(|e| RelicarioError::Kdf(e.to_string()))?;
|
||||
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||
|
||||
let mut output = [0u8; 32];
|
||||
argon2
|
||||
.hash_password_into(&password, salt, &mut output)
|
||||
.map_err(|e| IdfotoError::Kdf(e.to_string()))?;
|
||||
.map_err(|e| RelicarioError::Kdf(e.to_string()))?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
@@ -389,24 +389,24 @@ mod tests {
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core derive_master_key`
|
||||
Run: `cargo test -p relicario-core derive_master_key`
|
||||
Expected: All 3 tests PASS.
|
||||
|
||||
- [ ] **Step 5: Update lib.rs**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/lib.rs
|
||||
// crates/relicario-core/src/lib.rs
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
|
||||
pub use crypto::{derive_master_key, KdfParams};
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/
|
||||
git add crates/relicario-core/src/
|
||||
git commit -m "feat: add Argon2id key derivation with tests"
|
||||
```
|
||||
|
||||
@@ -415,11 +415,11 @@ git commit -m "feat: add Argon2id key derivation with tests"
|
||||
### Task 4: Crypto — Encrypt / Decrypt
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-core/src/crypto.rs`
|
||||
- Modify: `crates/relicario-core/src/crypto.rs`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Add to `crates/idfoto-core/src/crypto.rs` inside the `mod tests` block:
|
||||
Add to `crates/relicario-core/src/crypto.rs` inside the `mod tests` block:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
@@ -471,12 +471,12 @@ Add to `crates/idfoto-core/src/crypto.rs` inside the `mod tests` block:
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p idfoto-core encrypt`
|
||||
Run: `cargo test -p relicario-core encrypt`
|
||||
Expected: FAIL — `encrypt` and `decrypt` not defined.
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
Add to `crates/idfoto-core/src/crypto.rs`, above the `#[cfg(test)]` block:
|
||||
Add to `crates/relicario-core/src/crypto.rs`, above the `#[cfg(test)]` block:
|
||||
|
||||
```rust
|
||||
use chacha20poly1305::{
|
||||
@@ -503,7 +503,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, plaintext)
|
||||
.map_err(|_| IdfotoError::Encrypt("XChaCha20-Poly1305 encryption failed".into()))?;
|
||||
.map_err(|_| RelicarioError::Encrypt("XChaCha20-Poly1305 encryption failed".into()))?;
|
||||
|
||||
let mut output = Vec::with_capacity(1 + NONCE_SIZE + ciphertext.len());
|
||||
output.push(FORMAT_VERSION);
|
||||
@@ -518,7 +518,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
let min_len = 1 + NONCE_SIZE + 16; // version + nonce + tag (empty plaintext)
|
||||
if data.len() < min_len {
|
||||
return Err(IdfotoError::Format(format!(
|
||||
return Err(RelicarioError::Format(format!(
|
||||
"ciphertext too short: {} bytes, need at least {}",
|
||||
data.len(),
|
||||
min_len
|
||||
@@ -527,7 +527,7 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
let version = data[0];
|
||||
if version != FORMAT_VERSION {
|
||||
return Err(IdfotoError::Format(format!(
|
||||
return Err(RelicarioError::Format(format!(
|
||||
"unsupported format version: {version}"
|
||||
)));
|
||||
}
|
||||
@@ -538,7 +538,7 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
cipher
|
||||
.decrypt(nonce, ciphertext)
|
||||
.map_err(|_| IdfotoError::Decrypt)
|
||||
.map_err(|_| RelicarioError::Decrypt)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -557,24 +557,24 @@ use rand::RngCore;
|
||||
|
||||
- [ ] **Step 5: Run all crypto tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core`
|
||||
Run: `cargo test -p relicario-core`
|
||||
Expected: All tests PASS (3 KDF tests + 4 encrypt/decrypt tests).
|
||||
|
||||
- [ ] **Step 6: Update lib.rs exports**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/lib.rs
|
||||
// crates/relicario-core/src/lib.rs
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
|
||||
pub use crypto::{derive_master_key, encrypt, decrypt, KdfParams};
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/
|
||||
git add crates/relicario-core/src/
|
||||
git commit -m "feat: add XChaCha20-Poly1305 encrypt/decrypt with binary format"
|
||||
```
|
||||
|
||||
@@ -583,13 +583,13 @@ git commit -m "feat: add XChaCha20-Poly1305 encrypt/decrypt with binary format"
|
||||
### Task 5: Entry & Manifest Data Model
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/idfoto-core/src/entry.rs`
|
||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
||||
- Create: `crates/relicario-core/src/entry.rs`
|
||||
- Modify: `crates/relicario-core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write tests for serialization**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/entry.rs
|
||||
// crates/relicario-core/src/entry.rs
|
||||
|
||||
// ... (implementation in step 3)
|
||||
|
||||
@@ -663,13 +663,13 @@ mod tests {
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p idfoto-core entry`
|
||||
Run: `cargo test -p relicario-core entry`
|
||||
Expected: FAIL — types not defined.
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/entry.rs
|
||||
// crates/relicario-core/src/entry.rs
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -850,25 +850,25 @@ mod tests {
|
||||
- [ ] **Step 4: Update lib.rs**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/lib.rs
|
||||
// crates/relicario-core/src/lib.rs
|
||||
pub mod crypto;
|
||||
pub mod entry;
|
||||
pub mod error;
|
||||
|
||||
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
||||
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core entry`
|
||||
Run: `cargo test -p relicario-core entry`
|
||||
Expected: All 5 entry tests PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/
|
||||
git add crates/relicario-core/src/
|
||||
git commit -m "feat: add Entry, Manifest, ManifestEntry data model with serde"
|
||||
```
|
||||
|
||||
@@ -877,13 +877,13 @@ git commit -m "feat: add Entry, Manifest, ManifestEntry data model with serde"
|
||||
### Task 6: Vault Operations
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/idfoto-core/src/vault.rs`
|
||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
||||
- Create: `crates/relicario-core/src/vault.rs`
|
||||
- Modify: `crates/relicario-core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/vault.rs
|
||||
// crates/relicario-core/src/vault.rs
|
||||
|
||||
// ... (implementation in step 3)
|
||||
|
||||
@@ -955,13 +955,13 @@ mod tests {
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p idfoto-core vault`
|
||||
Run: `cargo test -p relicario-core vault`
|
||||
Expected: FAIL — functions not defined.
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/vault.rs
|
||||
// crates/relicario-core/src/vault.rs
|
||||
use crate::crypto;
|
||||
use crate::entry::{Entry, Manifest};
|
||||
use crate::error::Result;
|
||||
@@ -1061,7 +1061,7 @@ mod tests {
|
||||
- [ ] **Step 4: Update lib.rs**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/lib.rs
|
||||
// crates/relicario-core/src/lib.rs
|
||||
pub mod crypto;
|
||||
pub mod entry;
|
||||
pub mod error;
|
||||
@@ -1069,19 +1069,19 @@ pub mod vault;
|
||||
|
||||
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
||||
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
pub use vault::{decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest};
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run all tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core`
|
||||
Run: `cargo test -p relicario-core`
|
||||
Expected: All tests PASS (KDF + encrypt/decrypt + entry + vault).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/
|
||||
git add crates/relicario-core/src/
|
||||
git commit -m "feat: add vault encrypt/decrypt for entries and manifest"
|
||||
```
|
||||
|
||||
@@ -1090,15 +1090,15 @@ git commit -m "feat: add vault encrypt/decrypt for entries and manifest"
|
||||
### Task 7: imgsecret — JPEG Decode, Y Channel, Block DCT
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/idfoto-core/src/imgsecret.rs`
|
||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
||||
- Create: `crates/relicario-core/src/imgsecret.rs`
|
||||
- Modify: `crates/relicario-core/src/lib.rs`
|
||||
|
||||
This task builds the image-processing foundation. No embedding yet — just: load JPEG → extract luminance → divide into 8×8 blocks → DCT forward/inverse.
|
||||
|
||||
- [ ] **Step 1: Write tests for DCT round-trip and Y channel extraction**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/imgsecret.rs
|
||||
// crates/relicario-core/src/imgsecret.rs
|
||||
|
||||
// ... (implementation in step 3)
|
||||
|
||||
@@ -1179,14 +1179,14 @@ mod tests {
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p idfoto-core imgsecret`
|
||||
Run: `cargo test -p relicario-core imgsecret`
|
||||
Expected: FAIL — functions not defined.
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/imgsecret.rs
|
||||
use crate::error::{IdfotoError, Result};
|
||||
// crates/relicario-core/src/imgsecret.rs
|
||||
use crate::error::{RelicarioError, Result};
|
||||
use image::io::Reader as ImageReader;
|
||||
use std::f64::consts::PI;
|
||||
use std::io::Cursor;
|
||||
@@ -1214,11 +1214,11 @@ pub struct EmbedRegion {
|
||||
pub fn extract_y_channel(jpeg_bytes: &[u8]) -> Result<YChannel> {
|
||||
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
|
||||
let img = reader
|
||||
.decode()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
|
||||
let rgb = img.to_rgb8();
|
||||
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
|
||||
@@ -1464,7 +1464,7 @@ mod tests {
|
||||
- [ ] **Step 4: Update lib.rs**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/src/lib.rs
|
||||
// crates/relicario-core/src/lib.rs
|
||||
pub mod crypto;
|
||||
pub mod entry;
|
||||
pub mod error;
|
||||
@@ -1473,19 +1473,19 @@ pub mod vault;
|
||||
|
||||
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
||||
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
pub use vault::{decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest};
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core imgsecret`
|
||||
Run: `cargo test -p relicario-core imgsecret`
|
||||
Expected: All 4 tests PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/
|
||||
git add crates/relicario-core/src/
|
||||
git commit -m "feat: add imgsecret JPEG decode, Y channel extraction, and 8x8 DCT"
|
||||
```
|
||||
|
||||
@@ -1494,7 +1494,7 @@ git commit -m "feat: add imgsecret JPEG decode, Y channel extraction, and 8x8 DC
|
||||
### Task 8: imgsecret — QIM Embedding + Block Selection
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-core/src/imgsecret.rs`
|
||||
- Modify: `crates/relicario-core/src/imgsecret.rs`
|
||||
|
||||
This task adds QIM (Quantization Index Modulation) for embedding/extracting individual bits in DCT coefficients, and the fixed geometric pattern for selecting which blocks carry data.
|
||||
|
||||
@@ -1544,7 +1544,7 @@ Add to `mod tests` in `imgsecret.rs`:
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p idfoto-core qim`
|
||||
Run: `cargo test -p relicario-core qim`
|
||||
Expected: FAIL — `qim_embed`, `qim_extract`, `select_embed_blocks`, `QUANT_STEP` not defined.
|
||||
|
||||
- [ ] **Step 3: Write QIM and block selection implementation**
|
||||
@@ -1632,13 +1632,13 @@ pub fn select_embed_blocks(region: &EmbedRegion, target_count: usize) -> Vec<(us
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core imgsecret`
|
||||
Run: `cargo test -p relicario-core imgsecret`
|
||||
Expected: All tests PASS (previous 4 + 3 new QIM/block-selection tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/imgsecret.rs
|
||||
git add crates/relicario-core/src/imgsecret.rs
|
||||
git commit -m "feat: add QIM bit embedding and fixed-pattern block selection"
|
||||
```
|
||||
|
||||
@@ -1647,7 +1647,7 @@ git commit -m "feat: add QIM bit embedding and fixed-pattern block selection"
|
||||
### Task 9: imgsecret — Full embed() and extract()
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-core/src/imgsecret.rs`
|
||||
- Modify: `crates/relicario-core/src/imgsecret.rs`
|
||||
|
||||
This is the main event: the public `embed()` and `extract()` functions with redundancy coding and majority voting. Reed-Solomon is added in Task 10.
|
||||
|
||||
@@ -1697,7 +1697,7 @@ Add `use rand::Fill;` at the top of the test module for the random fill.
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p idfoto-core embed_extract`
|
||||
Run: `cargo test -p relicario-core embed_extract`
|
||||
Expected: FAIL — `embed` and `extract` not defined.
|
||||
|
||||
- [ ] **Step 3: Write embed() implementation**
|
||||
@@ -1727,7 +1727,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
|
||||
// Check minimum size
|
||||
if y.width < MIN_DIMENSION as usize || y.height < MIN_DIMENSION as usize {
|
||||
return Err(IdfotoError::ImageTooSmall {
|
||||
return Err(RelicarioError::ImageTooSmall {
|
||||
min_width: MIN_DIMENSION,
|
||||
min_height: MIN_DIMENSION,
|
||||
actual_width: y.width as u32,
|
||||
@@ -1739,7 +1739,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
let num_copies = (total_blocks / BLOCKS_PER_COPY).min(50); // cap at 50 copies
|
||||
|
||||
if num_copies < MIN_COPIES {
|
||||
return Err(IdfotoError::ImgSecret(format!(
|
||||
return Err(RelicarioError::ImgSecret(format!(
|
||||
"image too small for embedding: only {num_copies} copies fit, need at least {MIN_COPIES}"
|
||||
)));
|
||||
}
|
||||
@@ -1793,7 +1793,7 @@ fn extract_at_offset(jpeg_bytes: &[u8], dx: isize, dy: isize) -> Result<[u8; 32]
|
||||
let new_x = region.x_offset as isize + dx;
|
||||
let new_y = region.y_offset as isize + dy;
|
||||
if new_x < 0 || new_y < 0 {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
region.x_offset = new_x as usize;
|
||||
region.y_offset = new_y as usize;
|
||||
@@ -1808,7 +1808,7 @@ fn extract_at_offset(jpeg_bytes: &[u8], dx: isize, dy: isize) -> Result<[u8; 32]
|
||||
let num_copies = (total_blocks / BLOCKS_PER_COPY).min(50);
|
||||
|
||||
if num_copies < 1 {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
|
||||
let blocks_needed = num_copies * BLOCKS_PER_COPY;
|
||||
@@ -1857,7 +1857,7 @@ fn extract_at_offset(jpeg_bytes: &[u8], dx: isize, dy: isize) -> Result<[u8; 32]
|
||||
let total_votes: u32 = bit_votes.iter().map(|v| v[0] + v[1]).sum();
|
||||
let min_confidence = total_votes * 3 / 4; // at least 75% of votes should agree
|
||||
if confidence < min_confidence {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
|
||||
Ok(bits_to_bytes(&secret_bits))
|
||||
@@ -1894,11 +1894,11 @@ fn bits_to_bytes(bits: &[u8]) -> [u8; 32] {
|
||||
fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u8>> {
|
||||
let reader = ImageReader::new(Cursor::new(original_jpeg))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
|
||||
let img = reader
|
||||
.decode()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
|
||||
let rgb = img.to_rgb8();
|
||||
let (width, height) = (rgb.width(), rgb.height());
|
||||
@@ -1933,14 +1933,14 @@ fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u
|
||||
let encoder = JpegEncoder::new_with_quality(&mut buf, 92);
|
||||
encoder
|
||||
.write_image(output.as_raw(), width, height, image::ExtendedColorType::Rgb8)
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
|
||||
Ok(buf)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core imgsecret -- --nocapture`
|
||||
Run: `cargo test -p relicario-core imgsecret -- --nocapture`
|
||||
Expected: All tests PASS including embed/extract round-trip.
|
||||
|
||||
- [ ] **Step 5: Add a JPEG recompression survival test**
|
||||
@@ -1976,13 +1976,13 @@ Add to `mod tests`:
|
||||
|
||||
- [ ] **Step 6: Run all tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core`
|
||||
Run: `cargo test -p relicario-core`
|
||||
Expected: All tests PASS.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/imgsecret.rs
|
||||
git add crates/relicario-core/src/imgsecret.rs
|
||||
git commit -m "feat: add imgsecret embed/extract with redundancy and majority voting"
|
||||
```
|
||||
|
||||
@@ -1991,7 +1991,7 @@ git commit -m "feat: add imgsecret embed/extract with redundancy and majority vo
|
||||
### Task 10: imgsecret — Crop Recovery
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-core/src/imgsecret.rs`
|
||||
- Modify: `crates/relicario-core/src/imgsecret.rs`
|
||||
|
||||
- [ ] **Step 1: Write failing crop test**
|
||||
|
||||
@@ -2033,7 +2033,7 @@ Add to `mod tests`:
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p idfoto-core crop`
|
||||
Run: `cargo test -p relicario-core crop`
|
||||
Expected: FAIL — `extract_with_crop_recovery` not defined.
|
||||
|
||||
- [ ] **Step 3: Write crop recovery implementation**
|
||||
@@ -2066,7 +2066,7 @@ pub fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||
}
|
||||
}
|
||||
|
||||
Err(IdfotoError::ExtractionFailed)
|
||||
Err(RelicarioError::ExtractionFailed)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2110,19 +2110,19 @@ fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||
}
|
||||
}
|
||||
|
||||
Err(IdfotoError::ExtractionFailed)
|
||||
Err(RelicarioError::ExtractionFailed)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run all imgsecret tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core imgsecret -- --nocapture`
|
||||
Run: `cargo test -p relicario-core imgsecret -- --nocapture`
|
||||
Expected: All tests PASS including crop recovery.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/imgsecret.rs
|
||||
git add crates/relicario-core/src/imgsecret.rs
|
||||
git commit -m "feat: add crop recovery with multi-offset extraction search"
|
||||
```
|
||||
|
||||
@@ -2131,15 +2131,15 @@ git commit -m "feat: add crop recovery with multi-offset extraction search"
|
||||
### Task 11: CLI — Scaffolding, init, generate
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-cli/src/main.rs`
|
||||
- Modify: `crates/relicario-cli/src/main.rs`
|
||||
|
||||
- [ ] **Step 1: Write the clap CLI structure**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-cli/src/main.rs
|
||||
// crates/relicario-cli/src/main.rs
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use idfoto_core::{
|
||||
use relicario_core::{
|
||||
decrypt_entry, decrypt_manifest, derive_master_key, encrypt_entry, encrypt_manifest,
|
||||
generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry,
|
||||
};
|
||||
@@ -2148,7 +2148,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "idfoto", version, about = "Git-backed password manager with reference image authentication")]
|
||||
#[command(name = "relicario", version, about = "Git-backed password manager with reference image authentication")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
@@ -2230,21 +2230,21 @@ fn vault_dir() -> PathBuf {
|
||||
PathBuf::from(".")
|
||||
}
|
||||
|
||||
fn idfoto_dir() -> PathBuf {
|
||||
vault_dir().join(".idfoto")
|
||||
fn relicario_dir() -> PathBuf {
|
||||
vault_dir().join(".relicario")
|
||||
}
|
||||
|
||||
fn read_salt() -> Result<[u8; 32]> {
|
||||
let bytes = fs::read(idfoto_dir().join("salt"))
|
||||
.context("failed to read .idfoto/salt — is this a vault directory?")?;
|
||||
let bytes = fs::read(relicario_dir().join("salt"))
|
||||
.context("failed to read .relicario/salt — is this a vault directory?")?;
|
||||
let mut salt = [0u8; 32];
|
||||
salt.copy_from_slice(&bytes);
|
||||
Ok(salt)
|
||||
}
|
||||
|
||||
fn read_params() -> Result<KdfParams> {
|
||||
let json = fs::read_to_string(idfoto_dir().join("params.json"))
|
||||
.context("failed to read .idfoto/params.json")?;
|
||||
let json = fs::read_to_string(relicario_dir().join("params.json"))
|
||||
.context("failed to read .relicario/params.json")?;
|
||||
Ok(serde_json::from_str(&json)?)
|
||||
}
|
||||
|
||||
@@ -2256,7 +2256,7 @@ fn unlock(image_path: &Path) -> Result<[u8; 32]> {
|
||||
let jpeg_bytes = fs::read(image_path)
|
||||
.context("failed to read reference image")?;
|
||||
|
||||
let image_secret = idfoto_core::imgsecret::extract(&jpeg_bytes)
|
||||
let image_secret = relicario_core::imgsecret::extract(&jpeg_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("failed to extract image secret: {e}"))?;
|
||||
|
||||
let salt = read_salt()?;
|
||||
@@ -2268,9 +2268,9 @@ fn unlock(image_path: &Path) -> Result<[u8; 32]> {
|
||||
Ok(master_key)
|
||||
}
|
||||
|
||||
/// Get reference image path — from env var IDFOTO_IMAGE or prompt.
|
||||
/// Get reference image path — from env var RELICARIO_IMAGE or prompt.
|
||||
fn get_image_path() -> Result<PathBuf> {
|
||||
if let Ok(path) = std::env::var("IDFOTO_IMAGE") {
|
||||
if let Ok(path) = std::env::var("RELICARIO_IMAGE") {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
eprint!("Reference image path: ");
|
||||
@@ -2328,7 +2328,7 @@ fn cmd_init(image_path: &Path, output_path: &Path) -> Result<()> {
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut image_secret);
|
||||
|
||||
println!("Embedding secret into reference image...");
|
||||
let stego_jpeg = idfoto_core::imgsecret::embed(&carrier_jpeg, &image_secret)
|
||||
let stego_jpeg = relicario_core::imgsecret::embed(&carrier_jpeg, &image_secret)
|
||||
.map_err(|e| anyhow::anyhow!("failed to embed secret: {e}"))?;
|
||||
fs::write(output_path, &stego_jpeg)
|
||||
.context("failed to write reference image")?;
|
||||
@@ -2354,14 +2354,14 @@ fn cmd_init(image_path: &Path, output_path: &Path) -> Result<()> {
|
||||
.map_err(|e| anyhow::anyhow!("key derivation failed: {e}"))?;
|
||||
|
||||
// 5. Write vault structure
|
||||
fs::create_dir_all(idfoto_dir())?;
|
||||
fs::create_dir_all(relicario_dir())?;
|
||||
fs::create_dir_all(vault_dir().join("entries"))?;
|
||||
fs::write(idfoto_dir().join("salt"), salt)?;
|
||||
fs::write(relicario_dir().join("salt"), salt)?;
|
||||
fs::write(
|
||||
idfoto_dir().join("params.json"),
|
||||
relicario_dir().join("params.json"),
|
||||
serde_json::to_string_pretty(¶ms)?,
|
||||
)?;
|
||||
fs::write(idfoto_dir().join("devices.json"), "[]")?;
|
||||
fs::write(relicario_dir().join("devices.json"), "[]")?;
|
||||
|
||||
// 6. Write empty manifest
|
||||
let manifest = Manifest::new();
|
||||
@@ -2373,7 +2373,7 @@ fn cmd_init(image_path: &Path, output_path: &Path) -> Result<()> {
|
||||
// Add .gitignore
|
||||
fs::write(vault_dir().join(".gitignore"), "reference.jpg\n")?;
|
||||
|
||||
git_commit("feat: initialize idfoto vault")?;
|
||||
git_commit("feat: initialize relicario vault")?;
|
||||
|
||||
println!("\nVault initialized successfully!");
|
||||
println!("IMPORTANT: Keep your reference image ({}) safe — you need it to unlock the vault.", output_path.display());
|
||||
@@ -2664,7 +2664,7 @@ Expected: Shows all subcommands with descriptions.
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-cli/src/main.rs
|
||||
git add crates/relicario-cli/src/main.rs
|
||||
git commit -m "feat: add full CLI with init, add, get, list, edit, rm, sync, generate"
|
||||
```
|
||||
|
||||
@@ -2673,7 +2673,7 @@ git commit -m "feat: add full CLI with init, add, get, list, edit, rm, sync, gen
|
||||
### Task 12: CLI — Device Management
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-cli/src/main.rs`
|
||||
- Modify: `crates/relicario-cli/src/main.rs`
|
||||
|
||||
- [ ] **Step 1: Add device subcommands to the CLI**
|
||||
|
||||
@@ -2733,14 +2733,14 @@ struct DeviceEntry {
|
||||
}
|
||||
|
||||
fn read_devices() -> Result<Vec<DeviceEntry>> {
|
||||
let json = fs::read_to_string(idfoto_dir().join("devices.json"))
|
||||
let json = fs::read_to_string(relicario_dir().join("devices.json"))
|
||||
.context("failed to read devices.json")?;
|
||||
Ok(serde_json::from_str(&json)?)
|
||||
}
|
||||
|
||||
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
|
||||
let json = serde_json::to_string_pretty(devices)?;
|
||||
fs::write(idfoto_dir().join("devices.json"), json)?;
|
||||
fs::write(relicario_dir().join("devices.json"), json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2759,7 +2759,7 @@ fn cmd_device_add(name: &str) -> Result<()> {
|
||||
// Save private key to local config
|
||||
let config_dir = dirs::config_dir()
|
||||
.context("no config directory")?
|
||||
.join("idfoto");
|
||||
.join("relicario");
|
||||
fs::create_dir_all(&config_dir)?;
|
||||
fs::write(
|
||||
config_dir.join(format!("{name}.key")),
|
||||
@@ -2806,7 +2806,7 @@ fn cmd_device_revoke(name: &str) -> Result<()> {
|
||||
|
||||
- [ ] **Step 3: Add hex dependency**
|
||||
|
||||
Add to `crates/idfoto-cli/Cargo.toml` under `[dependencies]`:
|
||||
Add to `crates/relicario-cli/Cargo.toml` under `[dependencies]`:
|
||||
|
||||
```toml
|
||||
hex = "0.4"
|
||||
@@ -2824,7 +2824,7 @@ Expected: Compiles cleanly.
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-cli/
|
||||
git add crates/relicario-cli/
|
||||
git commit -m "feat: add device add/list/revoke commands with ed25519 key management"
|
||||
```
|
||||
|
||||
@@ -2833,16 +2833,16 @@ git commit -m "feat: add device add/list/revoke commands with ed25519 key manage
|
||||
### Task 13: Integration Test — Full Vault Workflow
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/idfoto-core/tests/integration.rs`
|
||||
- Create: `crates/relicario-core/tests/integration.rs`
|
||||
|
||||
This test exercises the full flow: generate secret → embed → derive key → encrypt entry → decrypt entry → extract secret from re-encoded image.
|
||||
|
||||
- [ ] **Step 1: Write the integration test**
|
||||
|
||||
```rust
|
||||
// crates/idfoto-core/tests/integration.rs
|
||||
use idfoto_core::*;
|
||||
use idfoto_core::imgsecret;
|
||||
// crates/relicario-core/tests/integration.rs
|
||||
use relicario_core::*;
|
||||
use relicario_core::imgsecret;
|
||||
|
||||
fn make_test_jpeg(width: u32, height: u32) -> Vec<u8> {
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
@@ -2967,7 +2967,7 @@ fn two_factor_independence() {
|
||||
|
||||
- [ ] **Step 2: Run integration tests**
|
||||
|
||||
Run: `cargo test -p idfoto-core --test integration`
|
||||
Run: `cargo test -p relicario-core --test integration`
|
||||
Expected: Both tests PASS.
|
||||
|
||||
- [ ] **Step 3: Run the full test suite**
|
||||
@@ -2978,7 +2978,7 @@ Expected: ALL tests across all crates PASS.
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/tests/
|
||||
git add crates/relicario-core/tests/
|
||||
git commit -m "test: add full-workflow integration test and two-factor independence verification"
|
||||
```
|
||||
|
||||
@@ -2987,7 +2987,7 @@ git commit -m "test: add full-workflow integration test and two-factor independe
|
||||
## Plan 2 Preview
|
||||
|
||||
After this plan is complete and passing, Plan 2 covers:
|
||||
- **idfoto-wasm**: wasm-bindgen wrapper around idfoto-core (compile with `wasm-pack build`)
|
||||
- **relicario-wasm**: wasm-bindgen wrapper around relicario-core (compile with `wasm-pack build`)
|
||||
- **Chrome MV3 extension**: TypeScript popup + content script + service worker, loading the WASM module for inline crypto
|
||||
- **Extension UX**: passphrase prompt, entry list/search, autofill detection
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# idfoto Credential Capture Implementation Plan
|
||||
# relicario Credential Capture Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
**Tech Stack:** TypeScript, Chrome extension APIs, DOM injection
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-credential-capture-design.md`
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-relicario-credential-capture-design.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -24,7 +24,7 @@ extension/src/popup/components/settings.ts # Settings view
|
||||
### Modified files
|
||||
|
||||
```
|
||||
extension/src/shared/types.ts # Add IdfotoSettings interface
|
||||
extension/src/shared/types.ts # Add RelicarioSettings interface
|
||||
extension/src/shared/messages.ts # Add new message types
|
||||
extension/src/service-worker/index.ts # Handle new messages
|
||||
extension/src/content/detector.ts # Import and init capture
|
||||
@@ -40,17 +40,17 @@ extension/src/popup/components/unlock.ts # Wire settings button to settings vie
|
||||
- Modify: `extension/src/shared/types.ts`
|
||||
- Modify: `extension/src/shared/messages.ts`
|
||||
|
||||
- [ ] **Step 1: Add IdfotoSettings to types.ts**
|
||||
- [ ] **Step 1: Add RelicarioSettings to types.ts**
|
||||
|
||||
Add at the end of `extension/src/shared/types.ts`:
|
||||
|
||||
```typescript
|
||||
export interface IdfotoSettings {
|
||||
export interface RelicarioSettings {
|
||||
captureEnabled: boolean;
|
||||
captureStyle: 'bar' | 'toast';
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: IdfotoSettings = {
|
||||
export const DEFAULT_SETTINGS: RelicarioSettings = {
|
||||
captureEnabled: false,
|
||||
captureStyle: 'bar',
|
||||
};
|
||||
@@ -64,7 +64,7 @@ Add these to the `Request` union in `extension/src/shared/messages.ts`:
|
||||
| { type: 'check_credential'; url: string; username: string; password: string }
|
||||
| { type: 'blacklist_site'; hostname: string }
|
||||
| { type: 'get_settings' }
|
||||
| { type: 'update_settings'; settings: Partial<import('./types').IdfotoSettings> }
|
||||
| { type: 'update_settings'; settings: Partial<import('./types').RelicarioSettings> }
|
||||
| { type: 'get_blacklist' }
|
||||
| { type: 'remove_blacklist'; hostname: string }
|
||||
```
|
||||
@@ -88,16 +88,16 @@ git commit -m "feat: add settings and credential capture message types"
|
||||
Add these helper functions to `extension/src/service-worker/index.ts`, after the existing storage helpers:
|
||||
|
||||
```typescript
|
||||
import type { IdfotoSettings } from '../shared/types';
|
||||
import type { RelicarioSettings } from '../shared/types';
|
||||
import { DEFAULT_SETTINGS } from '../shared/types';
|
||||
|
||||
async function loadSettings(): Promise<IdfotoSettings> {
|
||||
async function loadSettings(): Promise<RelicarioSettings> {
|
||||
const data = await chrome.storage.local.get(['settings']);
|
||||
if (!data.settings) return { ...DEFAULT_SETTINGS };
|
||||
return { ...DEFAULT_SETTINGS, ...data.settings };
|
||||
}
|
||||
|
||||
async function saveSettings(settings: IdfotoSettings): Promise<void> {
|
||||
async function saveSettings(settings: RelicarioSettings): Promise<void> {
|
||||
await chrome.storage.local.set({ settings });
|
||||
}
|
||||
|
||||
@@ -356,9 +356,9 @@ export function hookForms(): void {
|
||||
|
||||
// --- Prompt UI ---
|
||||
|
||||
/// Remove any existing idfoto prompt from the page.
|
||||
/// Remove any existing relicario prompt from the page.
|
||||
function removePrompt(): void {
|
||||
document.getElementById('idfoto-capture-prompt')?.remove();
|
||||
document.getElementById('relicario-capture-prompt')?.remove();
|
||||
}
|
||||
|
||||
/// Show a save/update prompt.
|
||||
@@ -385,7 +385,7 @@ function showPrompt(
|
||||
: `Save login for ${hostname}?`;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.id = 'idfoto-capture-prompt';
|
||||
container.id = 'relicario-capture-prompt';
|
||||
|
||||
// Common styles
|
||||
const baseStyles = `
|
||||
@@ -451,7 +451,7 @@ function showPrompt(
|
||||
|
||||
// Brand label
|
||||
const brand = document.createElement('span');
|
||||
brand.textContent = 'idfoto';
|
||||
brand.textContent = 'relicario';
|
||||
brand.style.cssText = 'color: #58a6ff; font-weight: normal; letter-spacing: 1px;';
|
||||
|
||||
// Message text
|
||||
@@ -627,7 +627,7 @@ Create `extension/src/popup/components/settings.ts`:
|
||||
/// Settings view — configure credential capture and manage blacklist.
|
||||
|
||||
import { setState, sendMessage, navigate, escapeHtml } from '../popup';
|
||||
import type { IdfotoSettings } from '../../shared/types';
|
||||
import type { RelicarioSettings } from '../../shared/types';
|
||||
|
||||
export async function renderSettings(app: HTMLElement): Promise<void> {
|
||||
// Load current settings and blacklist in parallel.
|
||||
@@ -636,8 +636,8 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
||||
sendMessage({ type: 'get_blacklist' }),
|
||||
]);
|
||||
|
||||
const settings: IdfotoSettings = settingsResp.ok
|
||||
? settingsResp.data as IdfotoSettings
|
||||
const settings: RelicarioSettings = settingsResp.ok
|
||||
? settingsResp.data as RelicarioSettings
|
||||
: { captureEnabled: false, captureStyle: 'bar' };
|
||||
|
||||
const blacklist: string[] = blacklistResp.ok
|
||||
@@ -1,4 +1,4 @@
|
||||
# idfoto Firefox Extension Port Implementation Plan
|
||||
# relicario Firefox Extension Port Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
**Tech Stack:** TypeScript, webpack, Firefox WebExtensions MV3
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-firefox-extension-design.md`
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-relicario-firefox-extension-design.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -46,12 +46,12 @@ Create `extension/manifest.firefox.json`:
|
||||
```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"
|
||||
}
|
||||
},
|
||||
@@ -84,8 +84,8 @@ Create `extension/manifest.firefox.json`:
|
||||
"setup.html",
|
||||
"setup.js",
|
||||
"styles.css",
|
||||
"idfoto_wasm_bg.wasm",
|
||||
"idfoto_wasm.js"
|
||||
"relicario_wasm_bg.wasm",
|
||||
"relicario_wasm.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -126,8 +126,8 @@ module.exports = {
|
||||
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
||||
{ from: 'setup.html', to: '.' },
|
||||
{ from: 'icons', to: 'icons' },
|
||||
{ from: 'wasm/idfoto_wasm_bg.wasm', to: '.' },
|
||||
{ from: 'wasm/idfoto_wasm.js', to: '.' },
|
||||
{ from: 'wasm/relicario_wasm_bg.wasm', to: '.' },
|
||||
{ from: 'wasm/relicario_wasm.js', to: '.' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -147,7 +147,7 @@ In `extension/package.json`, update the `scripts` section:
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -189,9 +189,9 @@ In `extension/src/service-worker/index.ts`, replace the current `initWasm` funct
|
||||
// (Chrome) and the default export (Firefox) are available.
|
||||
|
||||
// @ts-ignore TS2307 — resolved by webpack alias / copy
|
||||
import initDefault, { initSync } from '../../wasm/idfoto_wasm.js';
|
||||
import initDefault, { initSync } from '../../wasm/relicario_wasm.js';
|
||||
// @ts-ignore TS2307
|
||||
import * as wasmBindings from '../../wasm/idfoto_wasm.js';
|
||||
import * as wasmBindings from '../../wasm/relicario_wasm.js';
|
||||
|
||||
type WasmModule = typeof wasmBindings;
|
||||
let wasm: WasmModule | null = null;
|
||||
@@ -204,12 +204,12 @@ async function initWasm(): Promise<WasmModule> {
|
||||
|
||||
if (isServiceWorker) {
|
||||
// Chrome: fetch WASM binary and instantiate synchronously
|
||||
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 — dynamic init works
|
||||
const wasmUrl = chrome.runtime.getURL('idfoto_wasm_bg.wasm');
|
||||
const wasmUrl = chrome.runtime.getURL('relicario_wasm_bg.wasm');
|
||||
await initDefault(wasmUrl);
|
||||
}
|
||||
|
||||
@@ -225,13 +225,13 @@ async function initWasm(): Promise<WasmModule> {
|
||||
Change the doc comment at the top of the file (line 1) from:
|
||||
|
||||
```typescript
|
||||
/// Service worker entry point for the idfoto Chrome extension.
|
||||
/// Service worker entry point for the relicario Chrome extension.
|
||||
```
|
||||
|
||||
To:
|
||||
|
||||
```typescript
|
||||
/// Background script entry point for the idfoto browser extension.
|
||||
/// Background script entry point for the relicario browser extension.
|
||||
///
|
||||
/// In Chrome this runs as a service worker (MV3). In Firefox this runs
|
||||
/// as a persistent background script. WASM loading adapts automatically.
|
||||
@@ -1,14 +1,14 @@
|
||||
# idfoto Vault Initialization Wizard Implementation Plan
|
||||
# relicario Vault Initialization Wizard Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a browser-based wizard that creates a new idfoto vault, pushes it to Gitea/GitHub via API, downloads the reference image, and optionally configures the Chrome extension.
|
||||
**Goal:** Build a browser-based wizard that creates a new relicario vault, pushes it to Gitea/GitHub via API, downloads the reference image, and optionally configures the Chrome extension.
|
||||
|
||||
**Architecture:** Single HTML page (`extension/setup.html`) bundled by webpack as a new entry point. Reuses the existing git API layer and WASM module. New `embed_image_secret` function added to the WASM crate. The wizard runs entirely client-side — all crypto happens in the browser via WASM.
|
||||
|
||||
**Tech Stack:** TypeScript, wasm-bindgen (existing WASM crate), webpack, Chrome extension APIs
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md`
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-relicario-init-wizard-design.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
### Rust (modified)
|
||||
|
||||
```
|
||||
crates/idfoto-wasm/src/lib.rs # Add embed_image_secret function
|
||||
crates/relicario-wasm/src/lib.rs # Add embed_image_secret function
|
||||
```
|
||||
|
||||
### Extension (new)
|
||||
@@ -42,16 +42,16 @@ extension/manifest.json # Add web_accessible_resources for setup.html
|
||||
## Task 1: Add `embed_image_secret` to WASM Crate
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-wasm/src/lib.rs`
|
||||
- Modify: `crates/relicario-wasm/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write the test**
|
||||
|
||||
Add to the `#[cfg(test)] mod tests` block in `crates/idfoto-wasm/src/lib.rs`:
|
||||
Add to the `#[cfg(test)] mod tests` block in `crates/relicario-wasm/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn embed_then_extract_round_trip() {
|
||||
// Create a synthetic test JPEG (same approach as idfoto-core tests)
|
||||
// Create a synthetic test JPEG (same approach as relicario-core tests)
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::{ImageBuffer, ImageEncoder, Rgb};
|
||||
|
||||
@@ -81,12 +81,12 @@ fn embed_then_extract_round_trip() {
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p idfoto-wasm embed_then_extract`
|
||||
Run: `cargo test -p relicario-wasm embed_then_extract`
|
||||
Expected: FAIL — `embed_image_secret` not defined.
|
||||
|
||||
- [ ] **Step 3: Add `image` dev-dependency to Cargo.toml**
|
||||
|
||||
Add to `crates/idfoto-wasm/Cargo.toml` under `[dev-dependencies]`:
|
||||
Add to `crates/relicario-wasm/Cargo.toml` under `[dev-dependencies]`:
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
@@ -96,7 +96,7 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
||||
|
||||
- [ ] **Step 4: Implement the function**
|
||||
|
||||
Add to `crates/idfoto-wasm/src/lib.rs`, after the `extract_image_secret` function:
|
||||
Add to `crates/relicario-wasm/src/lib.rs`, after the `extract_image_secret` function:
|
||||
|
||||
```rust
|
||||
/// Embed a 256-bit secret into a carrier JPEG image.
|
||||
@@ -111,25 +111,25 @@ pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>,
|
||||
let secret: [u8; 32] = secret
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?;
|
||||
idfoto_core::imgsecret::embed(carrier_jpeg, &secret)
|
||||
relicario_core::imgsecret::embed(carrier_jpeg, &secret)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p idfoto-wasm embed_then_extract`
|
||||
Run: `cargo test -p relicario-wasm embed_then_extract`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Rebuild WASM**
|
||||
|
||||
Run: `wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm`
|
||||
Run: `wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm`
|
||||
Expected: Builds successfully.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-wasm/src/lib.rs crates/idfoto-wasm/Cargo.toml
|
||||
git add crates/relicario-wasm/src/lib.rs crates/relicario-wasm/Cargo.toml
|
||||
git commit -m "feat: add embed_image_secret to WASM crate"
|
||||
```
|
||||
|
||||
@@ -172,7 +172,7 @@ Create `extension/setup.html`:
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>idfoto — vault setup</title>
|
||||
<title>relicario — vault setup</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
/* Override popup constraints for full-page layout */
|
||||
@@ -339,12 +339,12 @@ let state: WizardState = {
|
||||
|
||||
// --- WASM ---
|
||||
|
||||
type WasmModule = typeof import('idfoto-wasm');
|
||||
type WasmModule = typeof import('relicario-wasm');
|
||||
let wasm: WasmModule | null = null;
|
||||
|
||||
async function initWasm(): Promise<WasmModule> {
|
||||
if (wasm) return wasm;
|
||||
const mod = await import(/* webpackIgnore: true */ '../idfoto_wasm.js');
|
||||
const mod = await import(/* webpackIgnore: true */ '../relicario_wasm.js');
|
||||
await mod.default();
|
||||
wasm = mod;
|
||||
return mod;
|
||||
@@ -378,7 +378,7 @@ function render(): void {
|
||||
const stepNames = ['git host', 'connection', 'create vault', 'done'];
|
||||
|
||||
let html = `
|
||||
<div class="brand" style="font-size:18px;margin-bottom:4px">idfoto setup</div>
|
||||
<div class="brand" style="font-size:18px;margin-bottom:4px">relicario setup</div>
|
||||
<div class="wizard-step">step ${state.step} of 4 — ${stepNames[state.step - 1]}</div>
|
||||
<div class="progress-bar"><div class="progress-bar-fill" style="width:${(state.step / 4) * 100}%"></div></div>
|
||||
`;
|
||||
@@ -416,7 +416,7 @@ function renderStep1(): string {
|
||||
<ol>
|
||||
<li>Log in to your Gitea instance</li>
|
||||
<li>Click <code>+</code> → <code>New Repository</code></li>
|
||||
<li>Name it (e.g. <code>idfoto-vault</code>), leave it <strong>empty</strong> — no README, no .gitignore</li>
|
||||
<li>Name it (e.g. <code>relicario-vault</code>), leave it <strong>empty</strong> — no README, no .gitignore</li>
|
||||
<li>Go to <code>Settings</code> → <code>Applications</code> → <code>Manage Access Tokens</code></li>
|
||||
<li>Generate a new token with <code>repo</code> scope (read/write)</li>
|
||||
<li>Copy the token — you'll need it in the next step</li>
|
||||
@@ -425,7 +425,7 @@ function renderStep1(): string {
|
||||
<div class="label" style="margin-bottom:8px">GITHUB SETUP</div>
|
||||
<ol>
|
||||
<li>Go to <strong>github.com</strong> → <code>New Repository</code></li>
|
||||
<li>Name it (e.g. <code>idfoto-vault</code>), set to <strong>Private</strong>, leave it <strong>empty</strong> — no README, no .gitignore, no license</li>
|
||||
<li>Name it (e.g. <code>relicario-vault</code>), set to <strong>Private</strong>, leave it <strong>empty</strong> — no README, no .gitignore, no license</li>
|
||||
<li>Go to <code>Settings</code> → <code>Developer Settings</code> → <code>Personal Access Tokens</code> → <code>Fine-grained tokens</code></li>
|
||||
<li>Click <code>Generate new token</code></li>
|
||||
<li>Select <strong>only</strong> the vault repository under "Repository access"</li>
|
||||
@@ -534,7 +534,7 @@ function renderStep4(): string {
|
||||
</p>
|
||||
` : `
|
||||
<p class="secondary" style="font-size:11px;margin-bottom:8px">
|
||||
idfoto extension detected. Push your vault config to it?
|
||||
relicario extension detected. Push your vault config to it?
|
||||
</p>
|
||||
<button class="btn" data-action="push-to-extension">Configure Extension</button>
|
||||
`}
|
||||
@@ -543,7 +543,7 @@ function renderStep4(): string {
|
||||
<div class="form-group" style="margin-top:16px">
|
||||
<div class="label">EXTENSION SETUP</div>
|
||||
<p class="secondary" style="font-size:11px;margin-bottom:8px">
|
||||
Install the idfoto extension, then enter these details in the setup wizard:
|
||||
Install the relicario extension, then enter these details in the setup wizard:
|
||||
</p>
|
||||
<div class="config-blob" data-action="copy-config" title="Click to copy">
|
||||
${escapeHtml(JSON.stringify({
|
||||
@@ -796,9 +796,9 @@ async function createVault(): Promise<void> {
|
||||
const manifestEnc = w.encrypt_manifest(emptyManifest, masterKey);
|
||||
|
||||
// 7. Push vault files to repo
|
||||
await git.writeFile('.idfoto/salt', salt, 'feat: initialize idfoto vault');
|
||||
await git.writeFile('.idfoto/params.json', new TextEncoder().encode(paramsJson), 'chore: add KDF params');
|
||||
await git.writeFile('.idfoto/devices.json', new TextEncoder().encode('[]'), 'chore: add empty devices list');
|
||||
await git.writeFile('.relicario/salt', salt, 'feat: initialize relicario vault');
|
||||
await git.writeFile('.relicario/params.json', new TextEncoder().encode(paramsJson), 'chore: add KDF params');
|
||||
await git.writeFile('.relicario/devices.json', new TextEncoder().encode('[]'), 'chore: add empty devices list');
|
||||
await git.writeFile('manifest.enc', new Uint8Array(manifestEnc), 'feat: add encrypted manifest');
|
||||
}
|
||||
|
||||
@@ -872,7 +872,7 @@ Add to `extension/manifest.json`, after the `content_security_policy` block:
|
||||
|
||||
```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>"]
|
||||
}]
|
||||
```
|
||||
@@ -901,7 +901,7 @@ git commit -m "feat: add setup wizard to webpack build and extension manifest"
|
||||
- [ ] **Step 1: Rebuild WASM**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rebuild extension**
|
||||
@@ -927,7 +927,7 @@ Expected: All tests pass (including the new `embed_then_extract_round_trip`).
|
||||
- Step 2: enter real host/token/repo, test connection works
|
||||
- Step 3: pick a JPEG, enter passphrase, create vault pushes files
|
||||
- Step 4: download reference image works, extension detection works
|
||||
4. Verify the vault repo now has `.idfoto/salt`, `.idfoto/params.json`, `.idfoto/devices.json`, `manifest.enc`
|
||||
4. Verify the vault repo now has `.relicario/salt`, `.relicario/params.json`, `.relicario/devices.json`, `manifest.enc`
|
||||
5. Open extension popup, unlock with passphrase — should work with the just-created vault
|
||||
|
||||
- [ ] **Step 5: Fix any issues found**
|
||||
@@ -1,14 +1,14 @@
|
||||
# idfoto WASM + Chrome MV3 Extension Implementation Plan
|
||||
# relicario WASM + Chrome MV3 Extension Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Compile `idfoto-core` to WASM and wrap it in a Chrome MV3 browser extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access.
|
||||
**Goal:** Compile `relicario-core` to WASM and wrap it in a Chrome MV3 browser extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access.
|
||||
|
||||
**Architecture:** Monolith service worker loads the WASM module and holds all state (master_key, cached manifest). Popup and content script are thin UI layers communicating via `chrome.runtime.sendMessage`. Vault data is fetched/committed directly via Gitea/GitHub REST APIs — no local clone, no CLI dependency.
|
||||
|
||||
**Tech Stack:** Rust + wasm-bindgen (WASM crate), TypeScript + webpack (extension), Chrome MV3 APIs
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-wasm-extension-design.md`
|
||||
**Spec:** `docs/superpowers/specs/2026-04-12-relicario-wasm-extension-design.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
### Rust (new crate)
|
||||
|
||||
```
|
||||
crates/idfoto-wasm/
|
||||
crates/relicario-wasm/
|
||||
├── Cargo.toml
|
||||
└── src/
|
||||
└── lib.rs # wasm-bindgen wrappers + TOTP implementation
|
||||
@@ -26,8 +26,8 @@ crates/idfoto-wasm/
|
||||
### Rust (modified)
|
||||
|
||||
```
|
||||
crates/idfoto-core/src/entry.rs # Add group field to Entry and ManifestEntry
|
||||
Cargo.toml # Add idfoto-wasm to workspace members
|
||||
crates/relicario-core/src/entry.rs # Add group field to Entry and ManifestEntry
|
||||
Cargo.toml # Add relicario-wasm to workspace members
|
||||
```
|
||||
|
||||
### Extension (all new)
|
||||
@@ -73,13 +73,13 @@ extension/
|
||||
## Task 0: Add Heavy Comments to Existing Rust Code
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
||||
- Modify: `crates/idfoto-core/src/error.rs`
|
||||
- Modify: `crates/idfoto-core/src/crypto.rs`
|
||||
- Modify: `crates/idfoto-core/src/entry.rs`
|
||||
- Modify: `crates/idfoto-core/src/vault.rs`
|
||||
- Modify: `crates/idfoto-core/src/imgsecret.rs`
|
||||
- Modify: `crates/idfoto-cli/src/main.rs`
|
||||
- Modify: `crates/relicario-core/src/lib.rs`
|
||||
- Modify: `crates/relicario-core/src/error.rs`
|
||||
- Modify: `crates/relicario-core/src/crypto.rs`
|
||||
- Modify: `crates/relicario-core/src/entry.rs`
|
||||
- Modify: `crates/relicario-core/src/vault.rs`
|
||||
- Modify: `crates/relicario-core/src/imgsecret.rs`
|
||||
- Modify: `crates/relicario-cli/src/main.rs`
|
||||
|
||||
Add thorough documentation comments to all existing Rust code. Every public function, struct, field, constant, and non-trivial private function should have doc comments explaining what it does, why it exists, and any important constraints. Module-level docs should explain the module's role in the overall architecture.
|
||||
|
||||
@@ -96,9 +96,9 @@ Guidelines:
|
||||
- [ ] **Step 1: Add module-level docs and comments to `lib.rs`**
|
||||
|
||||
```rust
|
||||
//! # idfoto-core
|
||||
//! # relicario-core
|
||||
//!
|
||||
//! Platform-agnostic core library for the idfoto password manager.
|
||||
//! Platform-agnostic core library for the relicario password manager.
|
||||
//!
|
||||
//! This crate is deliberately bytes-in/bytes-out — no filesystem, no network,
|
||||
//! no git operations. This makes it portable to WASM (browser extension),
|
||||
@@ -174,7 +174,7 @@ Expected: All tests pass unchanged.
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/ crates/idfoto-cli/src/main.rs
|
||||
git add crates/relicario-core/src/ crates/relicario-cli/src/main.rs
|
||||
git commit -m "docs: add heavy documentation comments to all Rust code"
|
||||
```
|
||||
|
||||
@@ -183,14 +183,14 @@ git commit -m "docs: add heavy documentation comments to all Rust code"
|
||||
## Task 1: Add `group` Field to Core Data Model
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/idfoto-core/src/entry.rs`
|
||||
- Modify: `crates/idfoto-core/src/vault.rs` (test helpers)
|
||||
- Modify: `crates/idfoto-cli/src/main.rs` (Entry construction sites)
|
||||
- Test: `crates/idfoto-core/src/entry.rs` (inline tests)
|
||||
- Modify: `crates/relicario-core/src/entry.rs`
|
||||
- Modify: `crates/relicario-core/src/vault.rs` (test helpers)
|
||||
- Modify: `crates/relicario-cli/src/main.rs` (Entry construction sites)
|
||||
- Test: `crates/relicario-core/src/entry.rs` (inline tests)
|
||||
|
||||
- [ ] **Step 1: Add `group` field to `Entry` struct**
|
||||
|
||||
In `crates/idfoto-core/src/entry.rs`, add the field after `totp_secret`:
|
||||
In `crates/relicario-core/src/entry.rs`, add the field after `totp_secret`:
|
||||
|
||||
```rust
|
||||
pub struct Entry {
|
||||
@@ -232,7 +232,7 @@ pub struct ManifestEntry {
|
||||
|
||||
Update every place that constructs `Entry` or `ManifestEntry` to include `group: None`. These are:
|
||||
|
||||
In `crates/idfoto-core/src/entry.rs` tests — `entry_serialization_round_trip`, `manifest_add_and_lookup`, `manifest_serialization_round_trip`, `manifest_search_case_insensitive`:
|
||||
In `crates/relicario-core/src/entry.rs` tests — `entry_serialization_round_trip`, `manifest_add_and_lookup`, `manifest_serialization_round_trip`, `manifest_search_case_insensitive`:
|
||||
|
||||
```rust
|
||||
// Every Entry construction gets:
|
||||
@@ -242,7 +242,7 @@ group: None,
|
||||
group: None,
|
||||
```
|
||||
|
||||
In `crates/idfoto-core/src/vault.rs` tests — `sample_entry()` helper and `manifest_encrypt_decrypt_round_trip`:
|
||||
In `crates/relicario-core/src/vault.rs` tests — `sample_entry()` helper and `manifest_encrypt_decrypt_round_trip`:
|
||||
|
||||
```rust
|
||||
// sample_entry() gets:
|
||||
@@ -252,7 +252,7 @@ group: None,
|
||||
group: None,
|
||||
```
|
||||
|
||||
In `crates/idfoto-core/tests/integration.rs` — `full_vault_workflow()` Entry construction (line ~55) and ManifestEntry (line ~101):
|
||||
In `crates/relicario-core/tests/integration.rs` — `full_vault_workflow()` Entry construction (line ~55) and ManifestEntry (line ~101):
|
||||
|
||||
```rust
|
||||
// Entry construction gets:
|
||||
@@ -262,7 +262,7 @@ group: None,
|
||||
group: None,
|
||||
```
|
||||
|
||||
In `crates/idfoto-cli/src/main.rs` — `cmd_add()` Entry construction (line ~328), `cmd_add()` ManifestEntry (line ~349), `cmd_edit()` Entry construction (line ~513), `cmd_edit()` ManifestEntry (line ~536):
|
||||
In `crates/relicario-cli/src/main.rs` — `cmd_add()` Entry construction (line ~328), `cmd_add()` ManifestEntry (line ~349), `cmd_edit()` Entry construction (line ~513), `cmd_edit()` ManifestEntry (line ~536):
|
||||
|
||||
```rust
|
||||
// Every Entry construction gets:
|
||||
@@ -274,7 +274,7 @@ group: None,
|
||||
|
||||
- [ ] **Step 4: Add a test for backwards compatibility (deserialize without group)**
|
||||
|
||||
In `crates/idfoto-core/src/entry.rs` tests:
|
||||
In `crates/relicario-core/src/entry.rs` tests:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
@@ -329,35 +329,35 @@ Expected: All tests pass, including new backwards-compatibility tests.
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-core/src/entry.rs crates/idfoto-core/src/vault.rs crates/idfoto-core/tests/integration.rs crates/idfoto-cli/src/main.rs
|
||||
git add crates/relicario-core/src/entry.rs crates/relicario-core/src/vault.rs crates/relicario-core/tests/integration.rs crates/relicario-cli/src/main.rs
|
||||
git commit -m "feat: add group field to Entry and ManifestEntry"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create `idfoto-wasm` Crate
|
||||
## Task 2: Create `relicario-wasm` Crate
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/idfoto-wasm/Cargo.toml`
|
||||
- Create: `crates/idfoto-wasm/src/lib.rs`
|
||||
- Create: `crates/relicario-wasm/Cargo.toml`
|
||||
- Create: `crates/relicario-wasm/src/lib.rs`
|
||||
- Modify: `Cargo.toml` (workspace members)
|
||||
|
||||
- [ ] **Step 1: Create Cargo.toml**
|
||||
|
||||
Create `crates/idfoto-wasm/Cargo.toml`:
|
||||
Create `crates/relicario-wasm/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "idfoto-wasm"
|
||||
name = "relicario-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WASM bindings for idfoto password manager"
|
||||
description = "WASM bindings for relicario password manager"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
idfoto-core = { path = "../idfoto-core" }
|
||||
relicario-core = { path = "../relicario-core" }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
serde_json = "1"
|
||||
@@ -371,21 +371,21 @@ wasm-bindgen-test = "0.3"
|
||||
|
||||
- [ ] **Step 2: Add to workspace**
|
||||
|
||||
In root `Cargo.toml`, add `"crates/idfoto-wasm"` to the members list:
|
||||
In root `Cargo.toml`, add `"crates/relicario-wasm"` to the members list:
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/idfoto-core",
|
||||
"crates/idfoto-cli",
|
||||
"crates/idfoto-wasm",
|
||||
"crates/relicario-core",
|
||||
"crates/relicario-cli",
|
||||
"crates/relicario-wasm",
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write the WASM wrapper**
|
||||
|
||||
Create `crates/idfoto-wasm/src/lib.rs`:
|
||||
Create `crates/relicario-wasm/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -399,7 +399,7 @@ pub fn derive_master_key(
|
||||
salt: &[u8],
|
||||
params_json: &str,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
let params: idfoto_core::KdfParams =
|
||||
let params: relicario_core::KdfParams =
|
||||
serde_json::from_str(params_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
let image_secret: [u8; 32] = image_secret
|
||||
@@ -409,7 +409,7 @@ pub fn derive_master_key(
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("salt must be exactly 32 bytes"))?;
|
||||
|
||||
let key = idfoto_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||
let key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
Ok(key.to_vec())
|
||||
@@ -421,7 +421,7 @@ pub fn encrypt(plaintext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
let key: [u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
idfoto_core::crypto::encrypt(&key, plaintext).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
relicario_core::crypto::encrypt(&key, plaintext).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Decrypt ciphertext with a 32-byte key. Returns plaintext bytes.
|
||||
@@ -430,14 +430,14 @@ pub fn decrypt(ciphertext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
let key: [u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
idfoto_core::crypto::decrypt(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
relicario_core::crypto::decrypt(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Extract a 256-bit secret from a JPEG with an embedded secret.
|
||||
#[wasm_bindgen]
|
||||
pub fn extract_image_secret(jpeg_bytes: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
let secret =
|
||||
idfoto_core::imgsecret::extract(jpeg_bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
relicario_core::imgsecret::extract(jpeg_bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(secret.to_vec())
|
||||
}
|
||||
|
||||
@@ -447,9 +447,9 @@ pub fn encrypt_entry(entry_json: &str, key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
let key: [u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
let entry: idfoto_core::Entry =
|
||||
let entry: relicario_core::Entry =
|
||||
serde_json::from_str(entry_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
idfoto_core::encrypt_entry(&key, &entry).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
relicario_core::encrypt_entry(&key, &entry).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Decrypt an entry from encrypted bytes. Returns JSON string.
|
||||
@@ -459,7 +459,7 @@ pub fn decrypt_entry(ciphertext: &[u8], key: &[u8]) -> Result<String, JsValue> {
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
let entry =
|
||||
idfoto_core::decrypt_entry(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
relicario_core::decrypt_entry(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
serde_json::to_string(&entry).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
@@ -469,9 +469,9 @@ pub fn encrypt_manifest(manifest_json: &str, key: &[u8]) -> Result<Vec<u8>, JsVa
|
||||
let key: [u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
let manifest: idfoto_core::Manifest =
|
||||
let manifest: relicario_core::Manifest =
|
||||
serde_json::from_str(manifest_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
idfoto_core::encrypt_manifest(&key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
relicario_core::encrypt_manifest(&key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Decrypt a manifest from encrypted bytes. Returns JSON string.
|
||||
@@ -480,7 +480,7 @@ pub fn decrypt_manifest(ciphertext: &[u8], key: &[u8]) -> Result<String, JsValue
|
||||
let key: [u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
let manifest = idfoto_core::decrypt_manifest(&key, ciphertext)
|
||||
let manifest = relicario_core::decrypt_manifest(&key, ciphertext)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
serde_json::to_string(&manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
@@ -618,27 +618,27 @@ mod tests {
|
||||
|
||||
- [ ] **Step 4: Verify it compiles**
|
||||
|
||||
Run: `cargo build -p idfoto-wasm`
|
||||
Run: `cargo build -p relicario-wasm`
|
||||
Expected: Compiles successfully.
|
||||
|
||||
- [ ] **Step 5: Run tests**
|
||||
|
||||
Run: `cargo test -p idfoto-wasm`
|
||||
Run: `cargo test -p relicario-wasm`
|
||||
Expected: All tests pass, including TOTP RFC 6238 test vectors.
|
||||
|
||||
- [ ] **Step 6: Test WASM compilation**
|
||||
|
||||
Run: `cargo install wasm-pack` (if not already installed), then:
|
||||
```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
|
||||
```
|
||||
Expected: Produces `extension/wasm/idfoto_wasm.js` and `extension/wasm/idfoto_wasm_bg.wasm`. Note the WASM binary size for later reference.
|
||||
Expected: Produces `extension/wasm/relicario_wasm.js` and `extension/wasm/relicario_wasm_bg.wasm`. Note the WASM binary size for later reference.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/idfoto-wasm/ Cargo.toml extension/wasm/
|
||||
git commit -m "feat: add idfoto-wasm crate with wasm-bindgen wrappers and TOTP"
|
||||
git add crates/relicario-wasm/ Cargo.toml extension/wasm/
|
||||
git commit -m "feat: add relicario-wasm crate with wasm-bindgen wrappers and TOTP"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -658,13 +658,13 @@ git commit -m "feat: add idfoto-wasm crate with wasm-bindgen wrappers and TOTP"
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "idfoto-extension",
|
||||
"name": "relicario-extension",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack --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",
|
||||
"build:all": "npm run build:wasm && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -682,13 +682,13 @@ Note: `@anthropic-ai/sdk` is NOT needed — remove that. The devDependencies sho
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "idfoto-extension",
|
||||
"name": "relicario-extension",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack --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",
|
||||
"build:all": "npm run build:wasm && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -706,13 +706,13 @@ Actually, strike that — no anthropic SDK. Final version:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "idfoto-extension",
|
||||
"name": "relicario-extension",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack --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",
|
||||
"build:all": "npm run build:wasm && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -781,8 +781,8 @@ module.exports = {
|
||||
{ from: 'src/popup/index.html', to: 'popup.html' },
|
||||
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
||||
{ from: 'icons', to: 'icons' },
|
||||
{ from: 'wasm/idfoto_wasm_bg.wasm', to: '.' },
|
||||
{ from: 'wasm/idfoto_wasm.js', to: '.' },
|
||||
{ from: 'wasm/relicario_wasm_bg.wasm', to: '.' },
|
||||
{ from: 'wasm/relicario_wasm.js', to: '.' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -797,7 +797,7 @@ module.exports = {
|
||||
```json
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "idfoto",
|
||||
"name": "relicario",
|
||||
"version": "0.1.0",
|
||||
"description": "Two-factor encrypted password manager",
|
||||
"permissions": ["storage", "activeTab", "clipboardWrite"],
|
||||
@@ -838,7 +838,7 @@ Create `extension/src/popup/index.html`:
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=360">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<title>idfoto</title>
|
||||
<title>relicario</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
@@ -1286,15 +1286,15 @@ import type { GitHost } from './git-host';
|
||||
import type { Entry, Manifest, ManifestEntry } from '../shared/types';
|
||||
|
||||
// These will be set by the service worker index after WASM init
|
||||
let wasm: typeof import('../../wasm/idfoto_wasm');
|
||||
let wasm: typeof import('../../wasm/relicario_wasm');
|
||||
|
||||
export function setWasm(w: typeof wasm) {
|
||||
wasm = w;
|
||||
}
|
||||
|
||||
export async function fetchVaultMeta(git: GitHost): Promise<{ salt: Uint8Array; paramsJson: string }> {
|
||||
const salt = await git.readFile('.idfoto/salt');
|
||||
const paramsBytes = await git.readFile('.idfoto/params.json');
|
||||
const salt = await git.readFile('.relicario/salt');
|
||||
const paramsBytes = await git.readFile('.relicario/params.json');
|
||||
const paramsJson = new TextDecoder().decode(paramsBytes);
|
||||
return { salt, paramsJson };
|
||||
}
|
||||
@@ -1414,13 +1414,13 @@ import {
|
||||
let masterKey: Uint8Array | null = null;
|
||||
let manifest: Manifest | null = null;
|
||||
let gitHost: GitHost | null = null;
|
||||
let wasm: typeof import('../../wasm/idfoto_wasm') | null = null;
|
||||
let wasm: typeof import('../../wasm/relicario_wasm') | null = null;
|
||||
|
||||
// ─── WASM initialization ───────────────────────────────────────────────────
|
||||
|
||||
async function initWasm(): Promise<typeof import('../../wasm/idfoto_wasm')> {
|
||||
async function initWasm(): Promise<typeof import('../../wasm/relicario_wasm')> {
|
||||
if (wasm) return wasm;
|
||||
const mod = await import(/* webpackIgnore: true */ './idfoto_wasm.js');
|
||||
const mod = await import(/* webpackIgnore: true */ './relicario_wasm.js');
|
||||
await mod.default();
|
||||
wasm = mod;
|
||||
setWasm(mod);
|
||||
@@ -2118,7 +2118,7 @@ import { sendMessage, navigate } from '../popup';
|
||||
|
||||
export function renderUnlock(container: HTMLElement) {
|
||||
container.innerHTML = `
|
||||
<div class="brand">idfoto</div>
|
||||
<div class="brand">relicario</div>
|
||||
<div style="margin-top: 16px">
|
||||
<div class="label">PASSPHRASE</div>
|
||||
<input type="password" id="passphrase" placeholder="Enter passphrase..." autofocus>
|
||||
@@ -2181,7 +2181,7 @@ import type { ManifestEntry } from '../../shared/types';
|
||||
export async function renderEntryList(container: HTMLElement) {
|
||||
container.innerHTML = `
|
||||
<div class="header">
|
||||
<div class="brand">idfoto</div>
|
||||
<div class="brand">relicario</div>
|
||||
<div class="status">🔓 unlocked</div>
|
||||
</div>
|
||||
<div class="search-bar">
|
||||
@@ -2702,7 +2702,7 @@ export function renderSetupWizard(container: HTMLElement) {
|
||||
|
||||
function render() {
|
||||
container.innerHTML = `
|
||||
<div class="brand">idfoto setup</div>
|
||||
<div class="brand">relicario setup</div>
|
||||
<div class="wizard-step">step ${step} of 3 — ${['repository', 'reference image', 'test unlock'][step - 1]}</div>
|
||||
<div class="progress-bar"><div class="progress-bar-fill" style="width: ${(step / 3) * 100}%"></div></div>
|
||||
<div id="wizard-content"></div>
|
||||
@@ -3128,10 +3128,10 @@ function showPicker(
|
||||
passwordField: HTMLInputElement
|
||||
) {
|
||||
// Remove any existing picker
|
||||
document.querySelectorAll('.idfoto-picker').forEach((el) => el.remove());
|
||||
document.querySelectorAll('.relicario-picker').forEach((el) => el.remove());
|
||||
|
||||
const picker = document.createElement('div');
|
||||
picker.className = 'idfoto-picker';
|
||||
picker.className = 'relicario-picker';
|
||||
picker.style.cssText = `
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -3222,7 +3222,7 @@ git commit -m "feat: add content script with form detection, field icon, and aut
|
||||
|
||||
```bash
|
||||
# Build WASM
|
||||
wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm
|
||||
wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm
|
||||
|
||||
# Install deps and build extension
|
||||
cd extension && npm install && npm run build
|
||||
@@ -3233,8 +3233,8 @@ Expected: `extension/dist/` contains all files needed to load as an unpacked Chr
|
||||
- [ ] **Step 2: Note the WASM binary size**
|
||||
|
||||
```bash
|
||||
ls -lh extension/wasm/idfoto_wasm_bg.wasm
|
||||
ls -lh extension/dist/idfoto_wasm_bg.wasm
|
||||
ls -lh extension/wasm/relicario_wasm_bg.wasm
|
||||
ls -lh extension/dist/relicario_wasm_bg.wasm
|
||||
```
|
||||
|
||||
Record the size for reference. If >2 MB uncompressed, consider optimizing later.
|
||||
@@ -3276,7 +3276,7 @@ git commit -m "feat: complete WASM + Chrome MV3 extension build"
|
||||
|------|-------------|--------------|
|
||||
| 0 | Add heavy comments to existing Rust code | None |
|
||||
| 1 | Add `group` field to core data model | Task 0 |
|
||||
| 2 | Create `idfoto-wasm` crate | Task 1 |
|
||||
| 2 | Create `relicario-wasm` crate | Task 1 |
|
||||
| 3 | Extension scaffolding | Task 2 |
|
||||
| 4 | Shared types and messages | Task 3 |
|
||||
| 5 | Git API layer | Task 4 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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**
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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>"]
|
||||
}]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user