886a0cd205b0
fix vault getRandomValues entropy overflow on >64KB vaults, defensive entries reload + celebration replay on license activation, tighten passkey approval card layout, bump v2.10.2
- time
- 2026-04-28 19:14 UTC · 6 months ago
- machine
- koh2
- parent
- 0527a541c2cf
↓ peach-browser-886a0cd2.tar.gzatlas at this save
files changed7 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/vault.ts+17 −1src/popup/App.tsx+15 −1src/popup/styles/popup.css+16 −5src/popup/views/PasskeyApproval.tsx+3 −3manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.10.1",4+ "version": "2.10.2",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.10.1",4+ "version": "2.10.2",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": "2.10.1",3+ "version": "2.10.2",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/vault.ts+17 −1
@@ -284,11 +284,27 @@284284 * ciphertext length matches the given byte count. Because GCM ciphertext is285285 * pseudo-random, this blob is indistinguishable at rest from a real286286 * encrypted vault without the key.287+ *288+ * crypto.getRandomValues() caps each call at 65536 bytes (the Web Crypto289+ * spec hard limit). Chunk the fill so vaults larger than that — common once290+ * the user has dozens of entries or any passkey — don't throw.287291 */292+const CSPRNG_CHUNK = 65_536;293+294+function fillRandom(buf: Uint8Array): Uint8Array {295+ for (let offset = 0; offset < buf.length; offset += CSPRNG_CHUNK) {296+ const end = Math.min(offset + CSPRNG_CHUNK, buf.length);297+ const chunk = new Uint8Array(end - offset);298+ crypto.getRandomValues(chunk);299+ buf.set(chunk, offset);300+ }301+ return buf;302+}303+288304 function generateNoiseBlob(ciphertextBytes: number): StoredVault {289305 const salt = crypto.getRandomValues(new Uint8Array(16));290306 const nonce = crypto.getRandomValues(new Uint8Array(12));291- const cipher = crypto.getRandomValues(new Uint8Array(ciphertextBytes));307+ const cipher = fillRandom(new Uint8Array(ciphertextBytes));292308 const tag = crypto.getRandomValues(new Uint8Array(16));293309 return {294310 version: 1,src/popup/App.tsx+15 −1
@@ -1076,7 +1076,21 @@10761076 onReduceMotionChange={applyReduceMotion}10771077 shortcuts={shortcutBindings()}10781078 onShortcutsChange={applyShortcuts}1079- onLicenseChange={refreshProStatus}1079+ onLicenseChange={async () => {1080+ // After activation/deactivation: refresh Pro status,1081+ // re-load entries (defensive — signal state can drift if1082+ // the SW briefly restarts during the verify+save round-trip),1083+ // and replay the Pro celebration if this is the first time1084+ // a valid Pro license is seen on this device.1085+ await refreshProStatus();1086+ void loadEntries();1087+ const should = await sendMessage({ type: "SHOULD_PLAY_CELEBRATION" });1088+ if ((should as { ok: boolean; data?: boolean }).data === true) {1089+ await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });1090+ setView("pro-celebration");1091+ window.setTimeout(() => setView("settings"), 2500);1092+ }1093+ }}10801094 onBack={() => { setSettingsInitialSection(null); setUpgradeHighlight(null); handleBack(); }}10811095 onImport={() => setView("import")}10821096 onExport={() => setView("export")}src/popup/styles/popup.css+16 −5
@@ -2048,8 +2048,8 @@20482048 display: flex;20492049 flex-direction: column;20502050 align-items: center;2051- padding: 28px 22px 22px;2052- gap: 14px;2051+ padding: 22px 20px 18px;2052+ gap: 10px;20532053 text-align: center;20542054 }20552055 @@ -2084,9 +2084,10 @@20842084 font-family: var(--font-display);20852085 font-style: italic;20862086 font-weight: 300;2087- font-size: 26px;2087+ font-size: 22px;20882088 color: var(--peach-t0);2089- line-height: 1;2089+ line-height: 1.1;2090+ margin: 2px 0;20902091 }20912092 20922093 .peach-passkey-approval-user {@@ -3888,7 +3889,17 @@38883889 38893890 .peach-passkey-card {38903891 max-width: 400px;3891- margin: auto;3892+ margin: auto 16px;3893+ align-self: center;3894+ width: calc(100% - 32px);3895+}3896+3897+/* Center the approval card vertically inside the overlay-view (which sets3898+ * flex-direction: column without justify-content). Without this, the card3899+ * sits at the top with a tall empty band below — looks unfinished on the3900+ * 320px popup. */3901+.peach-overlay-view:has(.peach-passkey-card) {3902+ justify-content: center;38923903 }38933904 38943905 .peach-pro-badge {src/popup/views/PasskeyApproval.tsx+3 −3
@@ -16,9 +16,9 @@1616 <div class="peach-overlay-card peach-passkey-card">1717 <div class="peach-passkey-approval">1818 <div class="peach-passkey-approval-icon" aria-hidden="true">19- <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">20- <circle cx="7" cy="7.5" r="4.25" />21- <path d="M10.5 10.75L17 17.25M14 14H17M15.5 12.5V15.5" />19+ <svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">20+ <circle cx="7" cy="11" r="3.5" />21+ <path d="M10.5 11h9M16.5 11v3M19.5 11v2" />2222 </svg>2323 </div>2424