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
↓ peach-browser-791712e7.tar.gzatlas at this save
files changed5 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 −0src/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 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>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,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 /**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");