3c4de4d66983

fix Mayan decoder (estimateModuleSize, findOthalan filter); fix passkey export in PeachScript/Mayan (entryTypeByte + ENTRY_TYPE_PASSKEY encoding for v2.11+ sub-object schema)

time
2026-05-01 14:18 UTC · 5 months ago
machine
koh2
parent
8fa1082f4aae
4 files manifest.firefox.json+1 −1manifest.json+1 −1src/crypto/mayan.ts+10 −20src/crypto/peachPayload.ts+25 −14
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.21.0",4+  "version": "2.22.0",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.21.0",4+  "version": "2.22.0",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

src/crypto/mayan.ts+10 −20
@@ -451,10 +451,11 @@451451       const dark2Len = x - dark2Start;452452 453453       if (454-        darkLen >= 2 &&455-        dark2Len >= 2 &&456-        Math.abs(darkLen - dark2Len) <= darkLen * 0.5 &&457-        lightLen > darkLen * 1.5454+        darkLen >= 4 &&455+        dark2Len >= 4 &&456+        Math.abs(darkLen - dark2Len) <= darkLen * 0.3 &&457+        lightLen > darkLen * 1.5 &&458+        lightLen < darkLen * 12458459       ) {459460         const cx = Math.floor((darkStart + x) / 2);460461         const size = x - darkStart;@@ -483,24 +484,13 @@483484 }484485 485486 function estimateModuleSize(486-  binary: boolean[][],487+  _binary: boolean[][],487488   rune: { cx: number; cy: number; size: number },488-  W: number,489+  _W: number,489490 ): number {490-  const y = rune.cy;491-  const strokeWidths: number[] = [];492-  for (let x = Math.max(0, rune.cx - rune.size); x < rune.cx + rune.size; x++) {493-    if (x >= W) break;494-    if (binary[y][x] && (x === 0 || !binary[y][x - 1])) {495-      let runLen = 0;496-      let xx = x;497-      while (xx < W && binary[y][xx]) { runLen++; xx++; }498-      if (runLen > 1 && runLen < rune.size / 2) strokeWidths.push(runLen);499-    }500-  }501-  if (strokeWidths.length === 0) return rune.size / 23;502-  const avg = strokeWidths.reduce((a, b) => a + b, 0) / strokeWidths.length;503-  return avg / 3;491+  // Rune spans ~23 modules at its widest horizontal cross-section.492+  // Round to integer: module boundaries are always integer pixels.493+  return Math.round(rune.size / 23);504494 }505495 506496 function detectRotation(

view file

src/crypto/peachPayload.ts+25 −14
@@ -471,21 +471,28 @@471471     }472472   } else if (type === ENTRY_TYPE_PASSKEY) {473473     // JWK + COSE + opaque IDs do not pack cleanly. Inline a JSON blob.474+    // v2.11+ stores the passkey in a nested sub-object; pre-v2.11 used flat fields.475+    const pk = entry.passkey;476+    const flat = entry as unknown as Record<string, unknown>;474477     const blob = encodeJsonSection({475-      rpId: entry.rpId,476-      rpName: entry.rpName,477-      credentialId: entry.credentialId,478-      publicKeyCose: entry.publicKeyCose,479-      privateKeyJwk: entry.privateKeyJwk,480-      userHandle: entry.userHandle,481-      userName: entry.userName,482-      userDisplayName: entry.userDisplayName,483-      algorithm: entry.algorithm,484-      signCount: entry.signCount,485-      aaguid: entry.aaguid,486-      backupEligible: entry.backupEligible,487-      backupState: entry.backupState,488-      lastUsedAt: entry.lastUsedAt,478+      rpId:             pk ? pk.rpId             : flat["rpId"],479+      rpName:           pk ? pk.rpName           : flat["rpName"],480+      credentialId:     pk ? pk.credentialId     : flat["credentialId"],481+      publicKeyCose:    pk ? pk.publicKeyCose    : flat["publicKeyCose"],482+      privateKeyJwk:    pk ? pk.privateKeyJwk    : flat["privateKeyJwk"],483+      userHandle:       pk ? pk.userHandle       : flat["userHandle"],484+      userName:         pk ? pk.userName         : flat["userName"],485+      userDisplayName:  pk ? pk.userDisplayName  : flat["userDisplayName"],486+      algorithm:        pk ? pk.algorithm        : flat["algorithm"],487+      signCount:        pk ? pk.signCount        : flat["signCount"],488+      aaguid:           pk ? pk.aaguid           : flat["aaguid"],489+      backupEligible:   pk ? pk.backupEligible   : flat["backupEligible"],490+      backupState:      pk ? pk.backupState      : flat["backupState"],491+      lastUsedAt:       pk ? pk.lastUsedAt       : flat["lastUsedAt"],492+      // Preserve login fields for passkey-bearing login entries.493+      ...(pk && entry.password  !== undefined ? { password:  entry.password  } : {}),494+      ...(pk && entry.url       !== undefined ? { url:       entry.url       } : {}),495+      ...(pk && entry.username  !== undefined ? { username:  entry.username  } : {}),489496     });490497     writer.u32(blob.length);491498     writer.bytes(blob);@@ -649,6 +656,10 @@649656 }650657 651658 function entryTypeByte(entry: VaultEntry): number {659+  // Passkey sub-object takes precedence — the v2.11+ schema stores passkeys660+  // as type:"login" entries with a nested passkey sub-object, so the type661+  // field alone is not sufficient to detect them.662+  if (entry.passkey) return ENTRY_TYPE_PASSKEY;652663   switch (entry.type) {653664     case "note":654665       return ENTRY_TYPE_NOTE;

view file