e0ec1767f8cc
v2.29.3 clean baseline — before android implementation spec
- time
- 2026-05-03 20:57 UTC · 5 months ago
- machine
- koh2
- parent
- 71bd41b42835
↓ peach-browser-e0ec1767.tar.gzatlas at this save
files changed1 file
audit2.29.3.md+275 −0audit2.29.3.md+275 −0
@@ -0,0 +1,275 @@1+# Peach v2.29.3 — Audit Report2+3+---4+5+## Section 1 — Sync: Dead Code Inventory6+7+### What exists8+9+**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`.10+11+**`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.**12+13+**`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.14+15+**Other sync files:**16+17+| 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 |26+27+**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).28+29+**Sync message types in `types.ts`:** 14+ sync/pairing message types — all relevant to the WebSocket path; keep them.30+31+### Verdict32+33+**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 manifests39+40+**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.42+43+**Keep intact:**44+- `noise-js.ts`, `sync-bridge.ts`, `capacitor-sync-transport.ts`, `android-sync-identity.ts`, `sync-coordinator.ts`, `SyncPairing.tsx`45+46+**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)51+52+---53+54+## Section 2 — Kotlin Plugins: What Exists and What's Wired55+56+All six plugins are registered in `MainActivity`: `AutofillPlugin`, `BiometricPlugin`, `InMemoryVaultPlugin`, `SyncPlugin`, `SyncKeyPlugin`, `FoldPlugin`.57+58+### AssistStructureParser.kt59+Helper class (no plugin methods). Traverses autofill `AssistStructure` to extract `usernameId`, `passwordId`, `webDomain`, and `packageName`. Used only by `PeachAutofillService`.60+61+### AutofillPlugin.kt — 5 plugin methods62+63+| 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` |70+71+Fully wired to JS. The `completeAutofillAuthentication()` path is complete — it reconstructs field IDs from intent extras and builds datasets from `VaultBridge.findEntries()`.72+73+### BiometricPlugin.kt — 2 plugin methods74+75+| Method | What it does |76+|---|---|77+| `authenticate()` | Shows `BiometricPrompt.PromptInfo`, resolves `{ verified: true }` or rejects |78+| `isAvailable()` | Checks `BIOMETRIC_STRONG` via `BiometricManager` |79+80+Correctly implemented on the Kotlin side. Wired to `src/shared/biometric.ts`. Gap is in JS layer (see Section 5).81+82+### FoldPlugin.kt — 2 plugin methods83+84+| Method | What it does |85+|---|---|86+| `startMonitoring()` | Starts `WindowInfoTracker` collector; emits `postureChanged` events |87+| `stopMonitoring()` | Cancels coroutine |88+89+Used in `App.tsx` via `startFoldMonitoring()`. Fully functional. Sets `data-posture` and `--peach-hinge-height` CSS variable.90+91+### InMemoryVaultPlugin.kt — 2 plugin methods92+93+| Method | What it does |94+|---|---|95+| `setEntries()` | Deserializes JSON entries into `InMemoryVault` singleton |96+| `clearEntries()` | Wipes `InMemoryVault` |97+98+Wired correctly. `InMemoryVault.kt` is the in-memory singleton — not a plugin itself.99+100+### SyncPlugin.kt — 8 plugin methods101+102+| 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 |112+113+Both sides of pairing implemented: enter-mode (UDP broadcast → `resolvePeerByCode`) and show-mode (TCP server + UDP beacon). Wired to `sync-bridge.ts`.114+115+### SyncKeyPlugin.kt — 3 plugin methods116+Stores X25519 keypair in `EncryptedSharedPreferences` (Keystore-backed AES256-GCM). Validates key length (64 hex = 32 bytes) on save. Wired to `android-sync-identity.ts`.117+118+### PeachAutofillService.kt (118 lines)119+System service, not a Capacitor plugin. Uses `VaultBridge` directly. See Section 6.120+121+### VaultBridge.kt122+Kotlin 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`.123+124+---125+126+## Section 3 — Mobile CSS: Leakage and Dead Code127+128+**42 occurrences** of `peach-mobile` in `popup.css`. All major layout overrides are properly scoped to `body.peach-mobile`.129+130+**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 JS135+136+None produce broken extension behavior because the classes are only applied from JS when `Capacitor.isNativePlatform()` is true.137+138+**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.139+140+**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.**141+142+**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.143+144+---145+146+## Section 4 — App.tsx: Mobile Wiring147+148+**`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.149+150+**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.151+152+**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`.153+154+---155+156+## Section 5 — Biometrics: Current Wiring157+158+**JS module (`src/shared/biometric.ts`):**159+- `biometricUnlock()` → `Biometric.authenticate()` → returns `result.verified`160+- `biometricAvailable()` → `Biometric.isAvailable()`161+162+**Kotlin side:** Correct. `authenticate()` shows a strong biometric prompt, resolves `{ verified: true }` on success.163+164+**Lock.tsx flow:**165+1. `BiometricUnlockButton` calls `handleBiometricUnlock(props)` on click166+2. `handleBiometricUnlock` calls `biometricUnlock()` — gets `true` on success167+3. On success, calls `props.onUnlock()` — which is App.tsx's `handleUnlock`168+169+**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.170+171+**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.172+173+**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.174+175+---176+177+## Section 6 — Autofill: Current State178+179+**`PeachAutofillService.kt` (118 lines):**180+181+*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.182+183+*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.184+185+*`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()`.186+187+**Inline suggestions (IME bar):** NOT implemented. Zero references to `InlineSuggestionsRequest`, `InlinePresentationSpec`, or `InlineSuggestion` anywhere in Kotlin.188+189+**"Create a new login" dataset:** NOT present. Only existing-match datasets are returned.190+191+**TOTP in autofill:** NOT handled. No OTP field detection.192+193+**`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.194+195+**Multiple datasets:** YES, up to 3 per fill request.196+197+---198+199+## Section 7 — Extension Health Check200+201+| 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 |209+210+The chunk size warning (`>500 kB after minification`) is consistent across all three web builds and is a bundle optimization note, not a correctness issue.211+212+---213+214+## Section 8 — Dead Code and Imports215+216+**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`219+220+**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)225+226+**Kotlin dead imports:** None — `NsdManager` and `WifiManager` in `SyncPlugin.kt` are in active use.227+228+**`SYN-001`:** Referenced only in `sync.ts` line 6 (the original gate comment). Not used elsewhere.229+230+**Safe to delete:**231+232+| 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 |240+241+**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)245+246+---247+248+## Section 9 — Master Spec vs. What Exists249+250+### 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 animation256+257+### 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.262+263+### 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 handled268+269+### Sync — Delete / Keep / Build270+271+| 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) |