1# Peach v2.29.3 — Audit Report23---45## Section 1 — Sync: Dead Code Inventory67### What exists89**Native host binary (`native/peach-sync-host/`):** EXISTS — full Rust crate with Cargo.lock, Cargo.toml, and `src/` containing `lib.rs`, `main.rs`, `mdns.rs`, `message.rs`, `noise.rs`, `tcp.rs`.1011**`nativeMessaging` permission:** Present in **both** `manifest.json` (line 55) and `manifest.firefox.json` (line 80). The comment in `sync.ts` explicitly says to keep it out of manifests until SYN-001 passes. **The permission is in the manifests prematurely.**1213**`src/background/sync.ts` (553 lines):** Marked `GATED` at the top — explicitly states it is not imported by any live background path. Contains `connectNative`, the full native host protocol, all message types.1415**Other sync files:**1617| File | Lines | Purpose |18|---|---|---|19| `src/background/sync-coordinator.ts` | 356 | Blob sync session logic — keep |20| `src/background/sync-lifecycle.ts` | 385 | Drives native host advertisement + mDNS lifecycle — imports sync.ts |21| `src/shared/sync-bridge.ts` | 238 | Android NSD bridge — keep |22| `src/shared/noise-js.ts` | 397 | Noise NK JS implementation — keep |23| `src/shared/capacitor-sync-transport.ts` | 226 | WebSocket transport for Android — keep |24| `src/shared/android-sync-identity.ts` | 90 | X25519 keypair management — keep |25| `src/popup/views/SyncPairing.tsx` | 323 | Pairing UI (show + enter modes) — keep |2627**Installer scripts:** `scripts/generate-native-sync-manifest.mjs`, `scripts/install-native-sync-host.mjs`, `scripts/sync-chrome-dist.mjs` (last one is unrelated to native host; keep it).2829**Sync message types in `types.ts`:** 14+ sync/pairing message types — all relevant to the WebSocket path; keep them.3031### Verdict3233**Delete cleanly:**34- `native/peach-sync-host/` — entire Rust directory35- `src/background/sync.ts` — native host client only, nothing here belongs to the WebSocket path36- `scripts/generate-native-sync-manifest.mjs`37- `scripts/install-native-sync-host.mjs`38- Remove `nativeMessaging` from both manifests3940**Delete with surgery:**41- `src/background/sync-lifecycle.ts` — currently imports and drives `sync.ts`; the mDNS advertisement it manages belongs to the native host. Either delete entirely and replace with a lightweight desktop-side sync coordinator, or strip the native host parts and keep only the blob-sync scheduling logic.4243**Keep intact:**44- `noise-js.ts`, `sync-bridge.ts`, `capacitor-sync-transport.ts`, `android-sync-identity.ts`, `sync-coordinator.ts`, `SyncPairing.tsx`4546**Build from scratch:**47- Mayan code (QR) pairing UI — not present at all48- `SCAN_PAIRING_QR` message handler49- Subnet scan for peer discovery50- Desktop-side sync-lifecycle replacement (decoupled from native host)5152---5354## Section 2 — Kotlin Plugins: What Exists and What's Wired5556All six plugins are registered in `MainActivity`: `AutofillPlugin`, `BiometricPlugin`, `InMemoryVaultPlugin`, `SyncPlugin`, `SyncKeyPlugin`, `FoldPlugin`.5758### AssistStructureParser.kt59Helper class (no plugin methods). Traverses autofill `AssistStructure` to extract `usernameId`, `passwordId`, `webDomain`, and `packageName`. Used only by `PeachAutofillService`.6061### AutofillPlugin.kt — 5 plugin methods6263| Method | What it does |64|---|---|65| `checkPendingSave()` | Reads `peach_pending_save` from SharedPreferences; consumed on read |66| `openAutofillSettings()` | Opens system autofill settings screen |67| `isEnabledAsAutofillService()` | Returns whether Peach is the active autofill service |68| `consumePendingAutofillRequest()` | Reads `autofill_domain`/`autofill_package` from intent; consumed on read |69| `completeAutofillAuthentication()` | After unlock, constructs `FillResponse` and returns via `EXTRA_AUTHENTICATION_RESULT` |7071Fully wired to JS. The `completeAutofillAuthentication()` path is complete — it reconstructs field IDs from intent extras and builds datasets from `VaultBridge.findEntries()`.7273### BiometricPlugin.kt — 2 plugin methods7475| Method | What it does |76|---|---|77| `authenticate()` | Shows `BiometricPrompt.PromptInfo`, resolves `{ verified: true }` or rejects |78| `isAvailable()` | Checks `BIOMETRIC_STRONG` via `BiometricManager` |7980Correctly implemented on the Kotlin side. Wired to `src/shared/biometric.ts`. Gap is in JS layer (see Section 5).8182### FoldPlugin.kt — 2 plugin methods8384| Method | What it does |85|---|---|86| `startMonitoring()` | Starts `WindowInfoTracker` collector; emits `postureChanged` events |87| `stopMonitoring()` | Cancels coroutine |8889Used in `App.tsx` via `startFoldMonitoring()`. Fully functional. Sets `data-posture` and `--peach-hinge-height` CSS variable.9091### InMemoryVaultPlugin.kt — 2 plugin methods9293| Method | What it does |94|---|---|95| `setEntries()` | Deserializes JSON entries into `InMemoryVault` singleton |96| `clearEntries()` | Wipes `InMemoryVault` |9798Wired correctly. `InMemoryVault.kt` is the in-memory singleton — not a plugin itself.99100### SyncPlugin.kt — 8 plugin methods101102| Method | What it does |103|---|---|104| `startDiscovery()` | NSD `_peach-sync._tcp` discovery with MulticastLock |105| `stopDiscovery()` | Stops NSD discovery |106| `getDiscoveredPeers()` | Returns resolved peer map |107| `resolvePeerByCode()` | UDP broadcast on port 44101 to locate peer by 6-digit token |108| `startPairingTcpServer()` | Opens `ServerSocket(0)`, pumps bytes as base64 `pairingChunk` events |109| `sendPairingChunk()` | Writes raw bytes to connected pairing socket |110| `startPairingBeacon()` | UDP listener on 44101; responds to `PEACH_PAIR_QUERY` with `PEACH_PAIR_RESPONSE` |111| `stopPairingServer()` | Closes all sockets, clears state |112113Both sides of pairing implemented: enter-mode (UDP broadcast → `resolvePeerByCode`) and show-mode (TCP server + UDP beacon). Wired to `sync-bridge.ts`.114115### SyncKeyPlugin.kt — 3 plugin methods116Stores X25519 keypair in `EncryptedSharedPreferences` (Keystore-backed AES256-GCM). Validates key length (64 hex = 32 bytes) on save. Wired to `android-sync-identity.ts`.117118### PeachAutofillService.kt (118 lines)119System service, not a Capacitor plugin. Uses `VaultBridge` directly. See Section 6.120121### VaultBridge.kt122Kotlin singleton. `isUnlocked()` delegates to `InMemoryVault.isAvailable()`. `findEntries()` has tiered domain matching: exact host (100) > base-domain (80) > subdomain (70/60) > package hint (50) > substring (40/25). Used by both `PeachAutofillService` and `AutofillPlugin`.123124---125126## Section 3 — Mobile CSS: Leakage and Dead Code127128**42 occurrences** of `peach-mobile` in `popup.css`. All major layout overrides are properly scoped to `body.peach-mobile`.129130**Potential leakage (not `body.peach-mobile`-scoped):**131- `.peach-mobile-root` (line 12) — applied to `<html>`, harmless; only set on native platform132- `.peach-mobile-row` (line 7093) — standalone utility class; would render in extension if applied in non-mobile context, but only used in mobile-specific components133- `.peach-mobile-status` (line 7100) — same; standalone class134- `.peach-onboarding-mobile-card-buttons` (line 6529) — standalone; only used in onboarding flow gated behind native platform checks in JS135136None produce broken extension behavior because the classes are only applied from JS when `Capacitor.isNativePlatform()` is true.137138**Panel/swipe navigation remnants:** NONE. No `.peach-panel`, `.peach-panels`, `300vw`, `VaultPanels`, or `activePanel` anywhere. The old 3-panel swipe layout has been completely removed.139140**Bottom sheet CSS:** Does NOT exist. Line 7284 has a comment about "long-press context menu + swipe-copy flash" — unrelated. No `.bottom-sheet`, `.peach-sheet`, drag, or swipe CSS for sheets. **The bottom sheet must be built from scratch.**141142**Entry row gesture CSS:** `.peach-entry-row` is standard list styling. The `transform: translateX/Y` values in the file are all fade-in/slide-in animations, not gesture responses.143144---145146## Section 4 — App.tsx: Mobile Wiring147148**`body.peach-mobile` on mount:** YES — lines 378–380, guarded by `Capacitor.isNativePlatform()`, applied in `onMount`. Both `document.body` and `document.documentElement` are classed correctly.149150**Autolock implementation:** The `appStateChange` listener (line 418) does NOT implement autolock. It only handles: (a) checking for a pending autofill save when the app foregrounds, and (b) re-checking autofill request extras. The `autoLockMinutes` signal (line 150) is read from settings (line 898) and passed as a prop to the Lock component (line 1502). There is no `backgroundedAt` timestamp and no setTimeout-based lock timer in `App.tsx`. Autolock either lives inside the Lock component (which receives the prop) or is not implemented.151152**Vault list on mobile:** `VaultView` at line 1481 handles the main entry view. A separate `view() === "list"` path at line 1480 renders a standalone list. No bottom sheet. On single-pane mobile, the vault opens directly to the entry view (no visible list). Two-pane layout (tablet) shows list + detail side by side via CSS overrides on `body.peach-mobile.layout-medium/layout-expanded`.153154---155156## Section 5 — Biometrics: Current Wiring157158**JS module (`src/shared/biometric.ts`):**159- `biometricUnlock()` → `Biometric.authenticate()` → returns `result.verified`160- `biometricAvailable()` → `Biometric.isAvailable()`161162**Kotlin side:** Correct. `authenticate()` shows a strong biometric prompt, resolves `{ verified: true }` on success.163164**Lock.tsx flow:**1651. `BiometricUnlockButton` calls `handleBiometricUnlock(props)` on click1662. `handleBiometricUnlock` calls `biometricUnlock()` — gets `true` on success1673. On success, calls `props.onUnlock()` — which is App.tsx's `handleUnlock`168169**The gap:** `props.onUnlock` is the *post*-unlock callback — the same one called after a successful password `UNLOCK` message. Biometric success calls this directly, bypassing the UNLOCK message entirely. **The vault is never decrypted.** No master password is retrieved; no derived key is stored or loaded from a biometric-protected keystore. Biometric auth returns `verified: true` and the app proceeds as if unlocked, but `vault.isUnlocked()` in the background is still false.170171**Result:** Biometric button appears, prompt shows, authentication succeeds, but the vault remains encrypted. The app's subsequent behavior (trying to read entries) will fail silently or show an empty vault.172173**Fix required:** Store the master password (or a vault-derived key) in an AndroidKeystore-backed `EncryptedSharedPreferences` entry, keyed to biometric auth. On successful `authenticate()`, retrieve that secret and send a real `UNLOCK` message with it.174175---176177## Section 6 — Autofill: Current State178179**`PeachAutofillService.kt` (118 lines):**180181*When locked:* Builds a single auth dataset — "Unlock Peach to autofill" — with a `PendingIntent` that launches `MainActivity`. The `autofill_domain`, `autofill_package`, and field IDs are passed as extras.182183*When unlocked:* Calls `VaultBridge.findEntries(domain ?? packageName)`. Returns up to 3 match datasets with title + username in the presentation view. Password is filled but not shown in presentation.184185*`onSaveRequest`:* Stub — calls `callback.onSuccess(null)` immediately. Save is handled via `VaultBridge.offerSave()` called from the autofill service's save path, which writes to SharedPreferences. App.tsx reads this on foreground via `checkPendingSave()`.186187**Inline suggestions (IME bar):** NOT implemented. Zero references to `InlineSuggestionsRequest`, `InlinePresentationSpec`, or `InlineSuggestion` anywhere in Kotlin.188189**"Create a new login" dataset:** NOT present. Only existing-match datasets are returned.190191**TOTP in autofill:** NOT handled. No OTP field detection.192193**`InMemoryVault` population:** Correctly wired. On vault open, `publishInMemoryVault(loadedEntries)` is called (App.tsx line 940) before `openVault()` completes. On lock, `clearInMemoryVault()` is called (lines 1101, 1608). `VaultBridge.isUnlocked()` checks `InMemoryVault.isAvailable()` — so the autofill service correctly knows vault state.194195**Multiple datasets:** YES, up to 3 per fill request.196197---198199## Section 7 — Extension Health Check200201| Target | Result |202|---|---|203| Tests | **311 passed**, 31 files, 0 failures |204| TypeScript (`--noEmit`) | **Clean** — 0 errors |205| `build:chrome` | **Success** (chunk size warning only, not an error) |206| `build:firefox` | **Success** (same warning) |207| `build:mobile` | **Success** (same warning) |208| Android `assembleDebug` | **BUILD SUCCESSFUL** in 4s, 34 tasks executed |209210The chunk size warning (`>500 kB after minification`) is consistent across all three web builds and is a bundle optimization note, not a correctness issue.211212---213214## Section 8 — Dead Code and Imports215216**Files importing `sync.ts` (native client):**217- `src/background/sync-lifecycle.ts` — the only live importer (`import * as sync from "./sync"`)218- `src/background/sync.test.ts` — test file mocking `connectNative`219220**Native host references in TypeScript:**221- `connectNative` — `sync.ts` lines 269, 273; test mock in `sync.test.ts`222- `peach-sync-host` — `sync.ts` header comment, `capacitor-sync-transport.ts` (protocol reference), `noise-js.ts` (auth proof reference), `native-sync-manifest-script.test.ts`223- `mDNS`/`mdns` — `sync-lifecycle.ts` (lines 4, 147, 361), `sync-bridge.ts` (comment)224- `NsdManager` — `SyncPlugin.kt` (actively used, not dead)225226**Kotlin dead imports:** None — `NsdManager` and `WifiManager` in `SyncPlugin.kt` are in active use.227228**`SYN-001`:** Referenced only in `sync.ts` line 6 (the original gate comment). Not used elsewhere.229230**Safe to delete:**231232| File/item | Reason |233|---|---|234| `native/peach-sync-host/` | Entire Rust binary; replaced by WebSocket path |235| `src/background/sync.ts` | Native host client; nothing reusable |236| `src/background/sync-lifecycle.ts` | Tightly coupled to sync.ts; needs replacement |237| `scripts/generate-native-sync-manifest.mjs` | Native host installer |238| `scripts/install-native-sync-host.mjs` | Native host installer |239| `nativeMessaging` in both manifests | Premature; no installer in place |240241**References to clean up after deletions:**242- `messages.ts` line 1227 — `connectNative` comment reference243- `sync-bridge.ts` — `NsdManager` comment reference (the Kotlin plugin it describes stays)244- `noise-js.ts` — `native/peach-sync-host/src/auth.rs` comment reference (keep the code, update the comment)245246---247248## Section 9 — Master Spec vs. What Exists249250### Vault List Bottom Sheet251- No bottom sheet CSS exists — not even a skeleton252- No bottom sheet JS component exists253- Mobile vault uses extension-style layout with `body.peach-mobile` overrides254- Single-pane mobile goes straight to the entry view; no swipeable list255- **Must be built entirely from scratch:** component, CSS, gesture handling, open/close animation256257### Biometrics258- `authenticate()` completes successfully — Kotlin is correct259- After authentication: calls the post-unlock callback without sending `UNLOCK` → vault not decrypted260- No derived key stored/retrieved; no biometric-protected keystore entry261- **Broken:** biometric succeeds but vault decryption never happens. Fix requires storing the master password (or a vault key) in an AndroidKeystore-backed secret, retrievable only after biometric auth, and using that key to perform the actual UNLOCK.262263### Autofill Inline (IME bar)264- `InlineSuggestionsRequest` — NOT present in any Kotlin file265- Multiple datasets: YES (up to 3 existing matches)266- "Create new login" dataset: NO267- TOTP: NOT handled268269### Sync — Delete / Keep / Build270271| Category | Items |272|---|---|273| **Delete** | `native/peach-sync-host/`, `sync.ts`, `sync-lifecycle.ts`, installer scripts, `nativeMessaging` from manifests |274| **Keep** | `noise-js.ts`, `sync-bridge.ts`, `capacitor-sync-transport.ts`, `android-sync-identity.ts`, `sync-coordinator.ts`, `SyncPairing.tsx`, all Kotlin sync plugins |275| **Build from scratch** | Mayan code scanning UI, `SCAN_PAIRING_QR` message handler, subnet scan for peer discovery (no JS or Kotlin implementation exists), new desktop sync-lifecycle (decoupled from native host) |