3cf91c205c62
persist derived key in chrome.storage.session; restore vault state on SW restart without user re-unlock, fix onConnect dead code (v3.6.27)
- time
- 2026-05-19 15:54 UTC · 5 months ago
- machine
- koh2
- parent
- 02cdb92bcff6
↓ peach-browser-3cf91c20.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/entry.ts+22 −0src/background/vault.ts+58 −0src/popup/components/settings/StackDeck.tsx+1 −1manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22 "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33 "manifest_version": 3,44 "name": "Peach",5- "version": "3.6.26",5+ "version": "3.6.27",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "3.6.26",4+ "version": "3.6.27",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.26",3+ "version": "3.6.27",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/entry.ts+22 −0
@@ -2,6 +2,10 @@22 import { setupAutoLock } from "./alarms";33 import { setupPasskeyProxy } from "./passkey";44 import { setupCommands } from "./commands";5+import { restoreFromSession, isUnlocked, lock } from "./vault";6+import { ensureSettingsLoaded } from "./vault";7+import { isRecoveryConfirmPending } from "./recovery-pending";8+import { denyAllPendingPasskeyRequests } from "./passkey";59 610 // Keep alive for MV3 service worker711 chrome.runtime.onInstalled.addListener(() => {});@@ -9,7 +13,25 @@913 // The chrome.idle.onStateChanged listener lives in ./alarms (the architectural1014 // home for auto-lock timing). Keeping it in only one place avoids drift.1115 16+// Lock-on-close via port disconnect. The popup opens a "popup" named port;17+// when that port disconnects (popup closed), lock if lockOnClose is enabled.18+// Previously declared in index.ts which was never bundled — moved here.19+chrome.runtime.onConnect.addListener((port) => {20+ if (port.name !== "popup") return;21+ port.onDisconnect.addListener(async () => {22+ await denyAllPendingPasskeyRequests();23+ const s = await ensureSettingsLoaded();24+ if (!isUnlocked() || s.lockOnClose === false) return;25+ if (await isRecoveryConfirmPending()) return;26+ lock();27+ });28+});29+1230 export function start() {31+ // Restore vault unlock state from chrome.storage.session if the MV3 service32+ // worker was restarted mid-session. Runs before message handlers are active33+ // so the first incoming message from the popup sees the correct unlock state.34+ void restoreFromSession();1335 setupMessageHandler();1436 void setupAutoLock();1537 setupCommands();src/background/vault.ts+58 −0
@@ -1045,6 +1045,7 @@10451045 10461046 if (payloadA && keyA) {10471047 derivedKey = keyA;1048+ void saveSessionKey(keyA, "real");10481049 vaultMode = "real";10491050 entries = payloadA.entries;10501051 duressConfigured = payloadA.duressConfigured;@@ -1090,6 +1091,7 @@10901091 10911092 if (payloadB && keyB) {10921093 derivedKey = keyB;1094+ void saveSessionKey(keyB, "decoy");10931095 vaultMode = "decoy";10941096 entries = payloadB.entries;10951097 duressConfigured = false;@@ -1112,7 +1114,63 @@11121114 return false;11131115 }11141116 1117+// ---------------------------------------------------------------------------1118+// MV3 service worker session key — survives SW restarts within a browser session1119+// ---------------------------------------------------------------------------1120+1121+// chrome.storage.session is cleared on browser close; survives SW restart.1122+// Firefox does not implement it (persistent background script, no restart risk).1123+const SESSION_KEY_STORAGE = "peach_session_dk";1124+1125+async function saveSessionKey(key: Uint8Array, mode: "real" | "decoy"): Promise<void> {1126+ if (!chrome.storage.session) return;1127+ await chrome.storage.session.set({ [SESSION_KEY_STORAGE]: { keyHex: hex(key), mode } });1128+}1129+1130+async function clearSessionKey(): Promise<void> {1131+ if (!chrome.storage.session) return;1132+ await chrome.storage.session.remove(SESSION_KEY_STORAGE);1133+}1134+1135+/**1136+ * On SW restart, re-derive all in-memory vault state from the session key1137+ * stored in chrome.storage.session (set at unlock time, cleared on lock).1138+ * Returns true if the vault was restored, false if no session key was found.1139+ * No-ops if the vault is already unlocked.1140+ */1141+export async function restoreFromSession(): Promise<boolean> {1142+ if (derivedKey !== null) return true;1143+ if (!chrome.storage.session) return false;1144+ const stored = await chrome.storage.session.get(SESSION_KEY_STORAGE);1145+ const entry = stored[SESSION_KEY_STORAGE] as { keyHex: string; mode: "real" | "decoy" } | undefined;1146+ if (!entry?.keyHex) return false;1147+1148+ const key = unhex(entry.keyHex);1149+ const slotKey = entry.mode === "real" ? SLOT_A : SLOT_B;1150+ const aadTag = entry.mode === "real" ? "slot_a_v1" : "slot_b_v1";1151+ const slot = await loadSlot(slotKey);1152+ if (!slot) { await clearSessionKey(); return false; }1153+1154+ const result = await tryDecryptSlotResult(slot, key, aadTag);1155+ if (result.status !== "ok") { await clearSessionKey(); return false; }1156+1157+ derivedKey = key;1158+ vaultMode = entry.mode;1159+ entries = result.payload.entries;1160+ duressConfigured = entry.mode === "real" ? result.payload.duressConfigured : false;1161+ buildDomainIndexSync(entries);1162+ await loadSettings();1163+ touchAutoLock();1164+1165+ if (entry.mode === "real" && slot.recovery_salt && slot.recovery_wrapped_root_key) {1166+ try { rootKey = await deriveRootKeyFromDerivedKey(key, slot); } catch { /* non-fatal */ }1167+ }1168+1169+ return true;1170+}1171+11151172 export function lock(): void {1173+ void clearSessionKey();11161174 if (derivedKey) {11171175 zeroKey(derivedKey);11181176 derivedKey = null;src/popup/components/settings/StackDeck.tsx+1 −1
@@ -1,4 +1,4 @@1-import { createMemo, createSignal, For, Show, onMount, onCleanup } from "solid-js";1+import { createMemo, createSignal, For, onMount, onCleanup } from "solid-js";22 import { StackCard } from "./StackCard";33 import type { CardId, StackBodies, StackCardSpec } from "./types";44