791712e7f33f

fix: PCH-02-003/005/006/010 doc-only patches — SAS note, PBKDF2 note + re-export prompt, vault_id privacy note, clipboard clear rationale (v4.0.0)

time
2026-06-12 20:36 UTC · 4 months ago
machine
koh2
agent
gpt-5
session
pch-02 integration
parent
1e1194c32494
5 files src/background/clipboard.ts+8 −1src/popup/views/Import.tsx+12 −0src/shared/backup.ts+12 −0src/shared/chirp-handshake.ts+10 −2src/shared/sync-identity.ts+7 −0
src/background/clipboard.ts+8 −1
@@ -16,7 +16,14 @@1616 //     with executeScript into the active tab as the Firefox fallback1717 //     (Firefox MV2/MV3 has no chrome.offscreen).1818 //   - A SHA-256 hash of the copied value (never the plaintext) is parked in19-//     chrome.storage.session so a restarted worker knows a clear is owed.19+//     chrome.storage.session so a restarted worker knows a clear is owed20+//     (PCH-01-021: the alarm handler skips offscreen churn when no clear is21+//     owed — the hash is load-bearing as an owed flag, not dead state).22+//   - PCH-02-010: the clear is deliberately unconditional. We do not read the23+//     clipboard to check it still holds our value because every viable read24+//     surface requires the `clipboardRead` permission ("Read data you copy and25+//     paste" in the store listing), a worse privacy posture than occasionally26+//     wiping a value the user copied later. Secret-must-not-persist wins.2027 2128 import { getSettings } from "./vault";2229 

view file

src/popup/views/Import.tsx+12 −0
@@ -423,6 +423,7 @@423423   const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);424424   const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);425425   const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);426+  const [legacyKdfNotice, setLegacyKdfNotice] = createSignal(false);426427   const [existingCount, setExistingCount] = createSignal<number>(0);427428 428429   const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);@@ -467,6 +468,7 @@467468     setBackupPassword("");468469     setError(null);469470     setAwaitingReplaceConfirm(false);471+    setLegacyKdfNotice(false);470472 471473     const text = await file.text();472474 @@ -541,6 +543,7 @@541543     setImporting(true);542544     setError(null);543545     setDuplicateWarning(0);546+    setLegacyKdfNotice(false);544547     setImportProgress(null);545548 546549     try {@@ -601,6 +604,7 @@601604       }602605 603606       setDuplicateWarning(dupes);607+      setLegacyKdfNotice(currentPreview.format === "peach-json");604608       await props.onImport();605609     } catch (err) {606610       const message = err instanceof Error ? err.message : String(err);@@ -791,6 +795,14 @@791795           <div class="peach-settings-group">792796             <div class="peach-export-card-copy">793797               {duplicateWarning()} {duplicateWarning() === 1 ? "entry appears" : "entries appear"} to be duplicates of existing ones. They were still imported — you can review and delete them in your vault.798+            </div>799+          </div>800+        </Show>801+802+        <Show when={legacyKdfNotice() && !importing()}>803+          <div class="peach-settings-group">804+            <div class="peach-restore-warning">805+              This backup was created with Peach's older encryption format (PBKDF2). It imported fine — but we recommend exporting a fresh backup now so your paper/file backups use the current, stronger format.794806             </div>795807           </div>796808         </Show>

view file

src/shared/backup.ts+12 −0
@@ -143,6 +143,18 @@143143   return { entries, ...(licenseKey ? { licenseKey, licenseActivatedAt } : {}) };144144 }145145 146+/**147+ * LEGACY IMPORT KDF (PCH-02-005):148+ * This path derives with PBKDF2-SHA256 (250,000 iterations) for backwards149+ * compatibility with `.json` exports sealed before the Argon2id migration.150+ * The encrypted-JSON export UI was removed in v2.5.0, so this helper is151+ * import-only, plus test fixture generation.152+ *153+ * PBKDF2 has no memory-hardness and is materially weaker than the current154+ * Argon2id envelope (m=64 MiB, t=3, p=4) against GPU/ASIC attackers at equal155+ * wall-clock cost. Do not use this helper for any new format. Users importing156+ * an old `.json` backup are prompted to export a fresh backup afterwards.157+ */146158 export async function derivePeachExportKey(147159   password: string,148160   salt: Uint8Array,

view file

src/shared/chirp-handshake.ts+10 −2
@@ -123,7 +123,7 @@123123 }124124 125125 // ───────────────────────────────────────────────────────────────────────────126-// SECURITY — TRUST MODEL: the pairing handshake does NOT cryptographically126+// SECURITY — TRUST MODEL (PCH-02-003): the pairing handshake does NOT cryptographically127127 // authenticate the initiator. Noise NK authenticates only the RESPONDER (it128128 // proves the initiator encrypted to the responder's "static" key — here the129129 // chirp ephemeral the responder BROADCAST in the clear over audio). On its own@@ -146,11 +146,19 @@146146 // OPEN ITEM (NOT verifiable from this repository): the Android/native client147147 // MUST surface and enforce the identical SAS gate before it persists the peer.148148 // If the app skips or auto-confirms the SAS, the channel is only as strong as149-// the app's weakest end — confirm the app-side gate against the Android source.149+// the app's weakest end — confirm the app-side gate against the Android source150+// at kepr.uk/peach-android before any release that touches pairing. A MITM who151+// intercepts the acoustic chirp and completes the handshake against a non-152+// enforcing peer can pair without user awareness.150153 // A future hardening step (coordinated wire change with the app) could add151154 // proof-of-possession via `performInitiatorAuth`/`performResponderAuth`152155 // (noise-js) so each side proves possession of the key it advertises, making153156 // authentication structural rather than dependent on the user's SAS comparison.157+//158+// This is documented residual risk in the cross-repo trust model, not a bug in159+// this codebase: the extension-side gateOnSAS is enforced here and regression-160+// tested (PCH-01-024); the peer-side gate lives in Android and must be re-161+// verified there on every pairing-touching Android release.154162 // ───────────────────────────────────────────────────────────────────────────155163 156164 /**

view file

src/shared/sync-identity.ts+7 −0
@@ -64,6 +64,13 @@6464  * first WebSocket handshake. Receiver rejects the connection if its6565  * own vault_id differs — prevents a paired-but-different-vault peer6666  * from corrupting the registry.67+ *68+ * PRIVACY NOTE (PCH-02-006): vault_id is stable across sessions and broadcast69+ * over mDNS during LAN sync discovery. A passive observer on the same network70+ * can link every device sharing this vault and observe sync activity. This is71+ * inherent to discovery and one-way — the master key is not recoverable from72+ * the BLAKE3 output. Users who need LAN unlinkability should leave sync73+ * disabled; "rotate sync identity" re-keys the root key and this identifier.6774  */6875 export async function computeVaultId(masterKey: Uint8Array): Promise<string> {6976   if (masterKey.length === 0) throw new Error("masterKey must not be empty");

view file