a31e6dadfa2a

mirror Android's manifest bounds and reject wsPort 0 so both clients judge a signed manifest identically (v5.2.36)

time
2026-09-10 17:45 UTC · 1 month ago
machine
koh2
agent
deepseek/deepseek-v4-flash
session
cross-client remediation
parent
2bbd5cf9fdad
3 files package.json+1 −1src/shared/device-manifest.test.ts+1 −12src/shared/device-manifest.ts+30 −2
package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.2.35",3+  "version": "5.2.36",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/shared/device-manifest.test.ts+1 −12
@@ -247,18 +247,7 @@247247   });248248 });249249 250-describe("verifyDeviceManifest accepts wsPort:0", () => {251-  it("does not reject wsPort:0 as malformed (Android Int default = no port)", async () => {252-    const kp = await deriveSyncSigningKeypair(MASTER_KEY, ISSUER_DEVICE_ID);253-    const e: ManifestDeviceEntry = {254-      ...entry(),255-      wsPort: 0,256-    };257-    const m = await buildSignedDeviceManifest(ISSUER_DEVICE_ID, [e], kp.privateKey);258-    const body = await verifyDeviceManifest(m, kp.publicKeyRaw);259-    expect(body.devices[0].wsPort).toBe(0);260-  });261-250+describe("verifyDeviceManifest accepts wsPort:null", () => {262251   it("does not reject wsPort:null on the wire (Android nullable Int → JSON null)", async () => {263252     const kp = await deriveSyncSigningKeypair(MASTER_KEY, ISSUER_DEVICE_ID);264253     // Build a valid manifest first, then inject wsPort:null into the body

view file

src/shared/device-manifest.ts+30 −2
@@ -79,6 +79,15 @@7979   );8080   return { body, signatureHex: bytesToHex(signature) };8181 }82+/**83+ * Manifest bounds, mirrored from Android's `DeviceManifestSupport`. A correctly84+ * signed manifest must be accepted or rejected identically by both clients, so85+ * these are contract, not defence-in-depth.86+ */87+const MAX_MANIFEST_DEVICES = 256;88+const MAX_MANIFEST_REVOCATIONS = 500;89+const MAX_MANIFEST_STRING = 256;90+8291 /** Canonical fields authenticated by an issuer for one revocation record. */8392 export function canonicalDeviceRevocation(record: DeviceRevocation): string {8493   return canonicalise({@@ -176,8 +185,24 @@176185   if (!Array.isArray(manifest.body.devices)) {177186     throw new DeviceManifestError("malformed", "devices is not an array");178187   }188+  if (189+    typeof manifest.body.issuerDeviceId !== "string" ||190+    manifest.body.issuerDeviceId.length === 0 ||191+    manifest.body.issuerDeviceId.length > MAX_MANIFEST_STRING192+  ) {193+    throw new DeviceManifestError("malformed", "issuerDeviceId invalid");194+  }195+  if (manifest.body.devices.length > MAX_MANIFEST_DEVICES) {196+    throw new DeviceManifestError("malformed", "devices exceeds MAX_MANIFEST_DEVICES");197+  }198+  if (!Array.isArray(manifest.body.revoked)) {199+    throw new DeviceManifestError("malformed", "revoked is not an array");200+  }201+  if (manifest.body.revoked.length > MAX_MANIFEST_REVOCATIONS) {202+    throw new DeviceManifestError("malformed", "revoked exceeds MAX_MANIFEST_REVOCATIONS");203+  }179204   for (const d of manifest.body.devices) {180-    if (typeof d.deviceId !== "string" || !d.deviceId) {205+    if (typeof d.deviceId !== "string" || !d.deviceId || d.deviceId.length > MAX_MANIFEST_STRING) {181206       throw new DeviceManifestError("malformed", "device.deviceId missing");182207     }183208     if (typeof d.deviceName !== "string" || !d.deviceName) {@@ -199,12 +224,15 @@199224     if (d.vouchDepth != null && (!Number.isInteger(d.vouchDepth) || d.vouchDepth < 0 || d.vouchDepth > MAX_VOUCH_DEPTH)) {200225       throw new DeviceManifestError("malformed", "device.vouchDepth invalid");201226     }227+    if (typeof d.pairedAt !== "number" || !Number.isFinite(d.pairedAt) || d.pairedAt < 0) {228+      throw new DeviceManifestError("malformed", "device.pairedAt invalid");229+    }202230     if (203231       d.wsPort != null &&204232       (205233         typeof d.wsPort !== "number" ||206234         !Number.isInteger(d.wsPort) ||207-        d.wsPort < 0 ||235+        d.wsPort < 1 ||208236         d.wsPort > 65535209237       )210238     ) {

view file