diff --git a/src/lib/export/__tests__/backup-key-ids.test.ts b/src/lib/export/__tests__/backup-key-ids.test.ts index 7de5089ab..03377c809 100644 --- a/src/lib/export/__tests__/backup-key-ids.test.ts +++ b/src/lib/export/__tests__/backup-key-ids.test.ts @@ -77,9 +77,12 @@ describe("innerCiphertextKeyId", () => { expect(innerCiphertextKeyId(legacy)?.keyId).toBe("v1"); }); - it("ignores an empty value and one that is not base64", () => { + it("ignores an empty value, a short one, and one that is not base64", () => { expect(innerCiphertextKeyId("")).toBeNull(); - expect(innerCiphertextKeyId("not ciphertext at all!")).toBeNull(); + expect(innerCiphertextKeyId("yes")).toBeNull(); + expect( + innerCiphertextKeyId("not ciphertext at all, and long enough to count!"), + ).toBeNull(); }); }); @@ -122,7 +125,9 @@ describe("assessBackupKeys", () => { useKeys({ old: K1, cur: K2 }, "cur"); const file = { moodEntries: [{ noteEncrypted: bytesString("fine") }], - appSettings: { adminAiKeyEncrypted: "gone.QUJDRA==" }, + appSettings: { + adminAiKeyEncrypted: `gone.${Buffer.alloc(40).toString("base64")}`, + }, }; const collector = new BackupKeyIdCollector(); collector.visit(file); diff --git a/src/lib/export/backup-key-ids.ts b/src/lib/export/backup-key-ids.ts index f3d614940..44b0ff0e7 100644 --- a/src/lib/export/backup-key-ids.ts +++ b/src/lib/export/backup-key-ids.ts @@ -40,6 +40,7 @@ const VERSIONED = /^([A-Za-z0-9_-]{1,32})\.[A-Za-z0-9+/=]+$/; const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/; const BASE64_TAIL = /^[A-Za-z0-9+/=]*$/; const BYTES_CODEC_VERSION = 0x02; +const MIN_CIPHERTEXT_CHARS = 40; /** How an inner value is laid out, which decides how a sample is opened. */ export type InnerCiphertextForm = "string" | "bytes-string" | "binary"; @@ -55,7 +56,10 @@ export interface InnerCiphertext { * multi-megabyte document costs the same as a note. */ export function innerCiphertextKeyId(value: string): InnerCiphertext | null { - if (value.length === 0) return null; + // The shortest ciphertext either codec writes is 28 bytes (nonce and tag), + // 40 characters as base64: anything shorter is not ciphertext, and reading + // it as a legacy value would demand a key for a field that holds none. + if (value.length < MIN_CIPHERTEXT_CHARS) return null; const versioned = VERSIONED.exec(value.slice(0, 64)); if (versioned && value.indexOf(".") === versioned[1].length) { return { keyId: versioned[1], form: "string" };