AUDIT_4.4.6.md
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.sessionto 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'), noeval, no remote scripts. The content script (which runs in page context) cannot accesschrome.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
lintscript. 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 insync-transport.tsbut 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 tofalseby Vite'sdefinein 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
deviceManifestVersionretain 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)
- Add a lint script (ESLint or Biome) to CI for ongoing hygiene.
- Consider pruning tombstones older than N months once all peers confirm receipt.
- Reduce console statements to structured debug logging behind a compile-time flag.
- Document the duress 256 KiB ceiling in user-facing materials for extreme vault sizes.
End of audit.