audit2.29.3.md

15.8 KB · raw

rendered · source

Peach v2.29.3 — Audit Report


Section 1 — Sync: Dead Code Inventory

What exists

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.

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.

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.

Other sync files:

File Lines Purpose
src/background/sync-coordinator.ts 356 Blob sync session logic — keep
src/background/sync-lifecycle.ts 385 Drives native host advertisement + mDNS lifecycle — imports sync.ts
src/shared/sync-bridge.ts 238 Android NSD bridge — keep
src/shared/noise-js.ts 397 Noise NK JS implementation — keep
src/shared/capacitor-sync-transport.ts 226 WebSocket transport for Android — keep
src/shared/android-sync-identity.ts 90 X25519 keypair management — keep
src/popup/views/SyncPairing.tsx 323 Pairing UI (show + enter modes) — keep

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).

Sync message types in types.ts: 14+ sync/pairing message types — all relevant to the WebSocket path; keep them.

Verdict

Delete cleanly:

Delete with surgery:

Keep intact:

Build from scratch:


Section 2 — Kotlin Plugins: What Exists and What's Wired

All six plugins are registered in MainActivity: AutofillPlugin, BiometricPlugin, InMemoryVaultPlugin, SyncPlugin, SyncKeyPlugin, FoldPlugin.

AssistStructureParser.kt

Helper class (no plugin methods). Traverses autofill AssistStructure to extract usernameId, passwordId, webDomain, and packageName. Used only by PeachAutofillService.

AutofillPlugin.kt — 5 plugin methods

Method What it does
checkPendingSave() Reads peach_pending_save from SharedPreferences; consumed on read
openAutofillSettings() Opens system autofill settings screen
isEnabledAsAutofillService() Returns whether Peach is the active autofill service
consumePendingAutofillRequest() Reads autofill_domain/autofill_package from intent; consumed on read
completeAutofillAuthentication() After unlock, constructs FillResponse and returns via EXTRA_AUTHENTICATION_RESULT

Fully wired to JS. The completeAutofillAuthentication() path is complete — it reconstructs field IDs from intent extras and builds datasets from VaultBridge.findEntries().

BiometricPlugin.kt — 2 plugin methods

Method What it does
authenticate() Shows BiometricPrompt.PromptInfo, resolves { verified: true } or rejects
isAvailable() Checks BIOMETRIC_STRONG via BiometricManager

Correctly implemented on the Kotlin side. Wired to src/shared/biometric.ts. Gap is in JS layer (see Section 5).

FoldPlugin.kt — 2 plugin methods

Method What it does
startMonitoring() Starts WindowInfoTracker collector; emits postureChanged events
stopMonitoring() Cancels coroutine

Used in App.tsx via startFoldMonitoring(). Fully functional. Sets data-posture and --peach-hinge-height CSS variable.

InMemoryVaultPlugin.kt — 2 plugin methods

Method What it does
setEntries() Deserializes JSON entries into InMemoryVault singleton
clearEntries() Wipes InMemoryVault

Wired correctly. InMemoryVault.kt is the in-memory singleton — not a plugin itself.

SyncPlugin.kt — 8 plugin methods

Method What it does
startDiscovery() NSD _peach-sync._tcp discovery with MulticastLock
stopDiscovery() Stops NSD discovery
getDiscoveredPeers() Returns resolved peer map
resolvePeerByCode() UDP broadcast on port 44101 to locate peer by 6-digit token
startPairingTcpServer() Opens ServerSocket(0), pumps bytes as base64 pairingChunk events
sendPairingChunk() Writes raw bytes to connected pairing socket
startPairingBeacon() UDP listener on 44101; responds to PEACH_PAIR_QUERY with PEACH_PAIR_RESPONSE
stopPairingServer() Closes all sockets, clears state

Both sides of pairing implemented: enter-mode (UDP broadcast → resolvePeerByCode) and show-mode (TCP server + UDP beacon). Wired to sync-bridge.ts.

SyncKeyPlugin.kt — 3 plugin methods

Stores X25519 keypair in EncryptedSharedPreferences (Keystore-backed AES256-GCM). Validates key length (64 hex = 32 bytes) on save. Wired to android-sync-identity.ts.

PeachAutofillService.kt (118 lines)

System service, not a Capacitor plugin. Uses VaultBridge directly. See Section 6.

VaultBridge.kt

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.


Section 3 — Mobile CSS: Leakage and Dead Code

42 occurrences of peach-mobile in popup.css. All major layout overrides are properly scoped to body.peach-mobile.

Potential leakage (not body.peach-mobile-scoped):

None produce broken extension behavior because the classes are only applied from JS when Capacitor.isNativePlatform() is true.

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.

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.

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.


Section 4 — App.tsx: Mobile Wiring

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.

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.

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.


Section 5 — Biometrics: Current Wiring

JS module (src/shared/biometric.ts):

Kotlin side: Correct. authenticate() shows a strong biometric prompt, resolves { verified: true } on success.

Lock.tsx flow:

  1. BiometricUnlockButton calls handleBiometricUnlock(props) on click
  2. handleBiometricUnlock calls biometricUnlock() — gets true on success
  3. On success, calls props.onUnlock() — which is App.tsx's handleUnlock

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.

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.

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.


Section 6 — Autofill: Current State

PeachAutofillService.kt (118 lines):

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.

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.

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().

Inline suggestions (IME bar): NOT implemented. Zero references to InlineSuggestionsRequest, InlinePresentationSpec, or InlineSuggestion anywhere in Kotlin.

"Create a new login" dataset: NOT present. Only existing-match datasets are returned.

TOTP in autofill: NOT handled. No OTP field detection.

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.

Multiple datasets: YES, up to 3 per fill request.


Section 7 — Extension Health Check

Target Result
Tests 311 passed, 31 files, 0 failures
TypeScript (--noEmit) Clean — 0 errors
build:chrome Success (chunk size warning only, not an error)
build:firefox Success (same warning)
build:mobile Success (same warning)
Android assembleDebug BUILD SUCCESSFUL in 4s, 34 tasks executed

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.


Section 8 — Dead Code and Imports

Files importing sync.ts (native client):

Native host references in TypeScript:

Kotlin dead imports: None — NsdManager and WifiManager in SyncPlugin.kt are in active use.

SYN-001: Referenced only in sync.ts line 6 (the original gate comment). Not used elsewhere.

Safe to delete:

File/item Reason
native/peach-sync-host/ Entire Rust binary; replaced by WebSocket path
src/background/sync.ts Native host client; nothing reusable
src/background/sync-lifecycle.ts Tightly coupled to sync.ts; needs replacement
scripts/generate-native-sync-manifest.mjs Native host installer
scripts/install-native-sync-host.mjs Native host installer
nativeMessaging in both manifests Premature; no installer in place

References to clean up after deletions:


Section 9 — Master Spec vs. What Exists

Vault List Bottom Sheet

Biometrics

Autofill Inline (IME bar)

Sync — Delete / Keep / Build

Category Items
Delete native/peach-sync-host/, sync.ts, sync-lifecycle.ts, installer scripts, nativeMessaging from manifests
Keep noise-js.ts, sync-bridge.ts, capacitor-sync-transport.ts, android-sync-identity.ts, sync-coordinator.ts, SyncPairing.tsx, all Kotlin sync plugins
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)