From 7f7e63333d2fe3bb510f6ea12024a3cf15027c7f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 22 Jun 2026 14:53:27 +0200 Subject: [PATCH] Use AES256 for V=5 documents with a mislabeled AESV2 crypt filter (bug 2046659) Some producers wrongly set the crypt filter CFM to AESV2 for V=5 documents; per the spec these must be decrypted with AES256 using the file encryption key directly. --- src/core/crypto.js | 6 +++++ test/unit/crypto_spec.js | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/core/crypto.js b/src/core/crypto.js index 53a7b7a42..744254e29 100644 --- a/src/core/crypto.js +++ b/src/core/crypto.js @@ -1280,6 +1280,12 @@ class CipherTransformFactory { PasswordResponses.NEED_PASSWORD ); } + if (this.algorithm === 5) { + // V=5 always uses 256-bit AES with the file encryption key, even + // when a producer wrongly sets the crypt filter's CFM to AESV2 + // (bug 2046659). + return AES256Cipher.bind(null, this.encryptionKey); + } if (cfm.name === "V2") { return ARCFourCipher.bind( null, diff --git a/test/unit/crypto_spec.js b/test/unit/crypto_spec.js index 8119b406f..1b3751280 100644 --- a/test/unit/crypto_spec.js +++ b/test/unit/crypto_spec.js @@ -597,6 +597,23 @@ describe("CipherTransformFactory", function () { expect(string).toEqual(decrypted); } + function ensureCrossCipherIsIdentity( + encryptDict, + decryptDict, + fileId, + password, + string + ) { + const encrypted = new CipherTransformFactory(encryptDict, fileId, password) + .createCipherTransform(123, 0) + .encryptString(string); + const decrypted = new CipherTransformFactory(decryptDict, fileId, password) + .createCipherTransform(123, 0) + .decryptString(encrypted); + + expect(decrypted).toEqual(string); + } + let fileId1, fileId2, dict1, dict2, dict3; let aes256Dict, aes256IsoDict, aes256BlankDict, aes256IsoBlankDict; @@ -853,6 +870,39 @@ describe("CipherTransformFactory", function () { "aaaaaaaaaaaaaaaaaaaaaa" ); }); + it("should decrypt V=5 with an AESV2 crypt filter using AES256", function () { + // Some producers wrongly set the crypt filter's CFM to AESV2 for V=5 + // documents, which must still be decrypted with AES256 (bug 2046659). + dict3.CF = buildDict({ + Identity: buildDict({ + CFM: Name.get("AESV3"), + }), + }); + const aesv3Dict = buildDict(dict3); + dict3.CF = buildDict({ + Identity: buildDict({ + CFM: Name.get("AESV2"), + }), + }); + const aesv2Dict = buildDict(dict3); + + for (const string of ["", "aaaa", "aaaaa", "aaaaaaaaaaaaaaaa"]) { + ensureCrossCipherIsIdentity( + aesv3Dict, + aesv2Dict, + fileId1, + "user", + string + ); + ensureCrossCipherIsIdentity( + aesv2Dict, + aesv3Dict, + fileId1, + "user", + string + ); + } + }); it("should encrypt and have the correct length using AES128", function () { dict3.CF = buildDict({ Identity: buildDict({