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.
|
||||
|
||||
Reference in New Issue
Block a user