d12b636cf3a3
require fresh master-password proof for cleartext vault export and bump v5.0.79
- time
- 2026-09-03 23:03 UTC · 1 month ago
- machine
- koh2
- agent
- claude-sonnet-4-6
- session
- PB-26-311 export reauthentication
- parent
- aaadb859cc90
↓ peach-browser-d12b636c.tar.gzatlas at this save
files changed9 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/message-policy.test.ts+1 −1src/background/message-policy.ts+1 −0src/background/messages.test.ts+21 −0src/background/messages.ts+11 −0src/popup/views/Export.tsx+11 −1src/shared/types.ts+1 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.0.78",4+ "version": "5.0.79",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.0.78",5+ "version": "5.0.79",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.0.78",3+ "version": "5.0.79",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/message-policy.test.ts+1 −1
@@ -13,7 +13,7 @@1313 const declaredTypes: MessageType[] = [1414 "GET_SYNC_ALERTS", "CLEAR_CLIPBOARD", "GET_PENDING_VOUCHED_DEVICES", "CONFIRM_VOUCHED_DEVICE",1515 "REJECT_VOUCHED_DEVICE", "VOUCHED_DEVICE_PENDING", "ROTATE_SYNC_IDENTITY", "DEVICE_KEY_ROTATED",16- "UNLOCK", "UNLOCK_WITH_PIN", "GET_PIN_STATUS", "LOCK", "IS_UNLOCKED", "HAS_VAULT", "GET_ENTRIES", "ADD_ENTRY",16+ "UNLOCK", "UNLOCK_WITH_PIN", "GET_PIN_STATUS", "LOCK", "IS_UNLOCKED", "HAS_VAULT", "GET_ENTRIES", "EXPORT_ENTRIES", "ADD_ENTRY",1717 "BATCH_ADD_ENTRIES", "RESTORE_ENTRIES", "CLEAR_VAULT_ENTRIES", "UPDATE_ENTRY", "ADD_ADDITIONAL_URL",1818 "REQUEST_REMEMBER_SITE",1919 "DELETE_ENTRY", "AUTOFILL_QUERY", "COPY", "CREATE_VAULT", "CREATE_VAULT_FROM_ROOT_KEY",src/background/message-policy.ts+1 −0
@@ -30,6 +30,7 @@3030 IS_UNLOCKED: "extension-page-only",3131 HAS_VAULT: "extension-page-only",3232 GET_ENTRIES: "extension-page-only",33+ EXPORT_ENTRIES: "extension-page-only",3334 BATCH_ADD_ENTRIES: "extension-page-only",3435 RESTORE_ENTRIES: "extension-page-only",3536 CLEAR_VAULT_ENTRIES: "extension-page-only",src/background/messages.test.ts+21 −0
@@ -1829,6 +1829,27 @@18291829 expect((res.data as string[]).length).toBe(12);18301830 });18311831 1832+ it("PB-26-311: cleartext export requires a fresh master-password proof", async () => {1833+ const vault = await import("./vault");1834+ await vault.createVault("correct-pass-12345");1835+1836+ await expect(1837+ dispatch({ type: "EXPORT_ENTRIES" as never, payload: {} }),1838+ ).resolves.toMatchObject({ ok: false, error: "password_required" });1839+ await expect(1840+ dispatch({1841+ type: "EXPORT_ENTRIES" as never,1842+ payload: { password: "not-the-password" },1843+ }),1844+ ).resolves.toMatchObject({ ok: false, error: "wrong_password" });1845+ await expect(1846+ dispatch({1847+ type: "EXPORT_ENTRIES" as never,1848+ payload: { password: "correct-pass-12345" },1849+ }),1850+ ).resolves.toMatchObject({ ok: true, data: [] });1851+ });1852+18321853 it("GET_PIN_STATUS reports only a session-available PIN wrapper", async () => {18331854 const vault = await import("./vault");18341855 const password = "correct-pass-12345";src/background/messages.ts+11 −0
@@ -567,6 +567,17 @@567567 case "GET_ENTRIES":568568 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };569569 return { ok: true, data: vault.getEntries() };570+ case "EXPORT_ENTRIES": {571+ if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };572+ const { password } = (msg.payload ?? {}) as { password?: unknown };573+ if (typeof password !== "string" || !password) {574+ return { ok: false, error: "password_required" };575+ }576+ if (!(await vault.verifyMainPassword(password))) {577+ return { ok: false, error: "wrong_password" };578+ }579+ return { ok: true, data: vault.getEntries() };580+ }570581 case "ADD_ENTRY": {571582 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };572583 const payload = msg.payload;src/popup/views/Export.tsx+11 −1
@@ -326,6 +326,7 @@326326 327327 function CSVExport(props: { onBack: () => void }) {328328 const [acknowledged, setAcknowledged] = createSignal(false);329+ const [masterPassword, setMasterPassword] = createSignal("");329330 const [working, setWorking] = createSignal(false);330331 const [error, setError] = createSignal<string | null>(null);331332 const [done, setDone] = createSignal<string | null>(null);@@ -335,7 +336,7 @@335336 setDone(null);336337 setWorking(true);337338 try {338- const res = await sendMessage({ type: "GET_ENTRIES" });339+ const res = await sendMessage({ type: "EXPORT_ENTRIES", payload: { password: masterPassword() } });339340 const entries = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];340341 // Passkeys are device-bound credentials and cannot be moved between341342 // devices via export. Strip the passkey sub-object out of any login@@ -389,6 +390,15 @@389390 </button>390391 </Show>391392 <Show when={acknowledged()}>393+ <label class="peach-settings-label" for="csv-export-password">master password</label>394+ <input395+ id="csv-export-password"396+ class="peach-settings-input"397+ type="password"398+ value={masterPassword()}399+ onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}400+ placeholder="enter your master password"401+ />392402 <button393403 class="peach-btn peach-btn--subtle peach-btn--sm"394404 onClick={() => void handleExport()}src/shared/types.ts+1 −0
@@ -212,6 +212,7 @@212212 | "IS_UNLOCKED"213213 | "HAS_VAULT"214214 | "GET_ENTRIES"215+ | "EXPORT_ENTRIES"215216 | "ADD_ENTRY"216217 | "BATCH_ADD_ENTRIES"217218 | "RESTORE_ENTRIES"