# Peach Browser Extension — Security & Reliability Audit **Version:** 4.4.6 **Audit date:** 2026-07-30 **Scope:** Full source audit (read-only), delta from v4.0.0 **Auditor:** Codex agent (GPT-5.6) **Prior audit:** v4.0.0 --- ## Executive Summary Peach v4.4.6 is **fit for production**. No critical or high-severity vulnerabilities were identified. The codebase demonstrates mature security engineering: a Rust-compiled Noise protocol core, per-entry authenticated encryption with key separation, constant-time comparisons at every authenticated site, Ed25519-signed device manifests with freshness windows, and a well-bounded sync receive path. The changes since v4.0.0 (recovery hardening, pairing/adoption, SAS verification, decentralized mesh convergence) are implemented safely and follow the established security patterns. --- ## Findings by Threat Level ### CRITICAL None identified. ### HIGH None identified. ### MEDIUM #### M-1: Session key persisted in `chrome.storage.session` (accepted risk) - **Location:** `src/background/vault.ts:1390–1410` - **Detail:** The AES-256 derived key is stored hex-encoded in `chrome.storage.session` to survive MV3 service-worker restarts. This storage is memory-backed and cleared on browser close, but a same-session XSS in an extension page (not a web page) could theoretically read it. - **Mitigation in place:** Extension pages have a strict CSP (`script-src 'self' 'wasm-unsafe-eval'; object-src 'none'`), no `eval`, no remote scripts. The content script (which runs in page context) cannot access `chrome.storage.session`. Destructive operations are gated to extension-origin senders. - **Verdict:** Accepted MV3 tradeoff. The alternative (re-prompt on every SW restart) is a worse UX with no meaningful security gain given the CSP posture. #### M-2: Tombstone accumulation without bound - **Location:** `src/background/sync-vault-adapter.ts:115–130` - **Detail:** Every deleted entry leaves a `{id, deletedAt}` tombstone that persists indefinitely and is sent in every sync session. A vault with tens of thousands of deletions over years will grow its sync manifest linearly. - **Mitigation in place:** `SYNC_LIMITS.MAX_TOMBSTONE_COUNT` (100,000) caps a single sync payload. Trash-retention purge eventually splices the entry array. Records are tiny (id + timestamp). - **Verdict:** Reliability concern for extreme long-term use, not a security issue. Documented in-code as a known limitation with a future pruning path. #### M-3: Recovery words encode 128 bits of a 256-bit root key - **Location:** `src/shared/recovery-words.ts:25–30`, `src/background/vault.ts:300–310` - **Detail:** The 12-word BIP39 phrase encodes only `rootKey[0..16]`. The recovery KEK is derived from a prefix of the plaintext it wraps. Effective recovery security is 128-bit, not 256-bit. - **Mitigation in place:** Documented extensively in-code (PCH-01-005). 128-bit security is the standard for BIP39 12-word phrases and is computationally infeasible to brute-force. The GCM ciphertext leaks nothing without the words. - **Verdict:** Intentional design, well-documented, standard security level for mnemonic recovery. ### LOW #### L-1: `console.log`/`console.warn` statements in production code (76 instances) - **Location:** Various (`src/background/`, `src/shared/`) - **Detail:** 76 console statements remain in non-test source. None log secrets (keys, passwords, entry content) — verified by inspection. They log sync stage names, error messages, and diagnostic counters. - **Verdict:** No information leakage risk. Cosmetic/hygiene issue only. #### L-2: No lint script configured - **Location:** `package.json` - **Detail:** The project has no `lint` script. Type checking (`tsc --noEmit`) and the full test suite (3,638 tests) pass, but there is no static analysis for common JS/TS pitfalls (unused vars, implicit any, etc.). - **Verdict:** Reliability hygiene gap, not a security issue. The strict TypeScript config and comprehensive test suite compensate substantially. #### L-3: `unlockWithKey` accepts a raw hex key without Argon2id - **Location:** `src/background/vault.ts:1466` - **Detail:** Used by Android biometric unlock where the key was derived natively. The browser extension never calls this path from user input — it is only reachable via the Capacitor/native bridge. - **Verdict:** Correct separation of concerns. The native side owns the KDF for biometric unlock. #### L-4: HIBP breach check uses SHA-1 (dictated by API) - **Location:** `src/background/vault.ts:230–250` - **Detail:** The Have I Been Pwned k-anonymity API requires SHA-1 prefixes. This is not a Peach design choice. Response padding is requested (`Add-Padding: true`). - **Verdict:** No action possible; the API dictates the hash. The k-anonymity model (5-char prefix only) means the full password hash never leaves the device. ### INFORMATIONAL #### I-1: Duress slot size ceiling (256 KiB) - **Location:** `src/background/vault.ts:70–75` - **Detail:** Both slots are padded to a fixed 256 KiB ceiling when duress is configured. A vault exceeding this would re-expose the duress bit via ciphertext length divergence. Documented as an extreme edge case. #### I-2: Subnet probe retired - **Location:** `src/background/sync-runner.ts:1–15` - **Detail:** LAN peer-to-peer sync is retired (`docs/sync-lan-retirement.md`). The subnet probe code remains in `sync-transport.ts` but is unreachable from the production runner. The relay is the sole transport. #### I-3: E2E passkey bypass compile-time flag - **Location:** `src/background/messages.ts:95`, `src/background/passkey.ts:25` - **Detail:** `__PEACH_E2E_PASSKEYS__` is folded to `false` by Vite's `define` in every production build. Verified in the bundled output by the build script. The gate is fully intact for users. --- ## Security Architecture Assessment ### Cryptography ✅ | Component | Implementation | Assessment | |-----------|---------------|------------| | KDF | Argon2id (m=64MiB, t=3, p=4) | Exceeds OWASP password-manager tier | | Vault encryption | AES-256-GCM, random 96-bit nonce, 128-bit tag, AAD domain separation | Correct | | Per-entry sync encryption | HKDF-SHA256 per entry ID, AES-GCM with entry-bound AAD | Prevents slot-swap attacks | | Key zeroing | `zeroKey()` on all ephemeral/derived keys after use | Consistent | | Self-test | PBKDF2+AES-GCM KAT on every unlock | Catches broken WebCrypto | | Cross-platform KAT | Argon2id vector coordinated with Android BouncyCastle | Prevents silent param drift | ### Protocol Security ✅ | Component | Implementation | Assessment | |-----------|---------------|------------| | Initial pairing | Noise_NK_25519_ChaChaPoly_BLAKE2s (Rust `snow` → WASM) | Correct pattern choice | | Reconnect | Noise_KK with both static keys, no NK fallback | Prevents downgrade | | Post-handshake auth | HMAC-SHA256 bound to handshake hash + DH + domain separation | Prevents session hijack | | SAS | 8-digit from handshake hash (64 PRF bits, negligible modulo bias) | Correct | | Device manifest | Ed25519 signed, canonical JSON, ±freshness window, version-pinned | Correct | | Revocation | Terminal, key-matched (not just id-matched), self-revocation filtered | Prevents re-admission | ### Input Validation ✅ - Sync frame size: 24 MiB hard cap (MV-3) - Entry count: 100,000 per payload - Entry plaintext: 512 KiB per entry - Tombstone count: 100,000 per payload - Timestamp clamping: ±5 minutes forward skew (HV-1) - Pairing invitation: 128-bit bearer token, base64url validated - Adoption frames: strict type guards on every field - Relay URLs: `wss:` only, no credentials in URL ### XSS / Injection Surface ✅ - Content script `innerHTML`: all assignments are static SVG strings or typed numbers — no page-derived data reaches markup - Note editor: allow-list sanitizer (12 tags, all attributes stripped) - Popup: SolidJS (auto-escaping by default), no `dangerouslySetInnerHTML` - CSP: `script-src 'self' 'wasm-unsafe-eval'; object-src 'none'` - Message handler: sender validation, destructive ops gated to extension origin ### Reliability ✅ - 164 test files, 3,638 passing tests + 40 todo - Typecheck passes (`tsc --noEmit`) - Chrome/Firefox/Mobile production builds pass - MV3 static-import guard (build fails on runtime `import()` in worker graph) - Clipboard clear: eviction-proof (chrome.alarms + offscreen document + retry) - Auto-lock: alarm-based + idle detection, recovery-confirm exemption - Sync timeout: 30s per session, stage-level diagnostics - Mesh listener: bounded retry with exponential backoff, abort on lock --- ## Delta Assessment (v4.0.0 → v4.4.6) | Change Area | Versions | Safety Verdict | |-------------|----------|----------------| | Recovery flow hardening (try/finally, canvas ref fix) | 4.0.x | ✅ Safe | | Onboarding reveal toggle + copy | 4.0.x | ✅ Safe | | Playwright smoke tests (recovery, popup lifecycle) | 4.0.x | ✅ Safe | | Pairing/adoption visual QA (QR, SAS, success states) | 4.4.4–4.4.5 | ✅ Safe (CSS/layout only) | | CSS cascade fix (compact selector specificity) | 4.4.5 | ✅ Safe | | Decentralized mesh convergence (deviceManifestVersion: 3) | 4.4.6 | ✅ Safe | | Vouched trust source promotion (direct/vouched → equal authority) | 4.4.6 | ✅ Safe | | SYNC_DEVICE_MANIFEST on equal entry versions | 4.4.6 | ✅ Safe | The mesh convergence change (v4.4.6) is the highest-risk delta. It was implemented correctly: - Capability-gated: peers that omit `deviceManifestVersion` retain the legacy fast path - Authority model: any admitted, Noise-authenticated peer with a valid signing key may propagate; unknown/revoked keys remain blocked - Revocation ordering: absorbed before vouching in the same manifest to prevent race - Self-revocation filtered: a peer cannot revoke the receiver's own device --- ## Production Worthiness **Verdict: APPROVED FOR PRODUCTION** The codebase demonstrates: - Defense-in-depth at every trust boundary (transport + envelope encryption) - Constant-time comparisons at all authenticated sites - Strict input validation with bounded resource consumption - Comprehensive test coverage (3,638 tests) with adversarial and KAT suites - Clean separation between the content-script attack surface and the privileged background - Documented security model with inline rationale at every non-obvious decision - No hardcoded secrets, no eval, no remote code loading - MV3 compliance with static-import enforcement No blocking issues. The MEDIUM findings are accepted design tradeoffs with appropriate mitigations. The LOW findings are hygiene items that do not affect security posture. --- ## Recommendations (non-blocking) 1. Add a lint script (ESLint or Biome) to CI for ongoing hygiene. 2. Consider pruning tombstones older than N months once all peers confirm receipt. 3. Reduce console statements to structured debug logging behind a compile-time flag. 4. Document the duress 256 KiB ceiling in user-facing materials for extreme vault sizes. --- *End of audit.*