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
7 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 −3
manifest.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": {

view file

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",

view file

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": {

view file

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,

view file

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")}

view file

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 {

view file

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 

view file