diff --git a/src/core/crypto.js b/src/core/crypto.js index 8197b36ca..4074aa634 100644 --- a/src/core/crypto.js +++ b/src/core/crypto.js @@ -1407,11 +1407,12 @@ class CipherTransform { // Append some chars equal to "16 - (M mod 16)" // where M is the string length (see section 7.6.2 in PDF specification) // to have a final string where the length is a multiple of 16. + // Special note: + // "Note that the pad is present when M is evenly divisible by 16; + // it contains 16 bytes of 0x10." const strLen = s.length; const pad = 16 - (strLen % 16); - if (pad !== 16) { - s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad)); - } + s += String.fromCharCode(pad).repeat(pad); // Generate an initialization vector const iv = new Uint8Array(16); diff --git a/test/unit/crypto_spec.js b/test/unit/crypto_spec.js index 6acacf4d4..0230d623a 100644 --- a/test/unit/crypto_spec.js +++ b/test/unit/crypto_spec.js @@ -581,6 +581,30 @@ describe("CipherTransformFactory", function () { } } + function ensureAESEncryptedStringHasCorrectLength( + dict, + fileId, + password, + string + ) { + const factory = new CipherTransformFactory(dict, fileId, password); + const cipher = factory.createCipherTransform(123, 0); + const encrypted = cipher.encryptString(string); + + // The final length is a multiple of 16. + // If the initial string has a length which is a multiple of 16 + // then 16 chars of padding are added. + // So we've the mapping: + // - length: [0-15] => new length: 16 + // - length: [16-31] => new length: 32 + // - length: [32-47] => new length: 48 + // ... + expect(encrypted.length).toEqual( + 16 /* initialization vector length */ + + 16 * Math.ceil((string.length + 1) / 16) + ); + } + function ensureEncryptDecryptIsIdentity(dict, fileId, password, string) { const factory = new CipherTransformFactory(dict, fileId, password); const cipher = factory.createCipherTransform(123, 0); @@ -807,6 +831,8 @@ describe("CipherTransformFactory", function () { }), }); const dict = buildDict(dict3); + // 0 char + ensureEncryptDecryptIsIdentity(dict, fileId1, "user", ""); // 1 char ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "a"); // 2 chars @@ -828,6 +854,8 @@ describe("CipherTransformFactory", function () { }), }); const dict = buildDict(dict3); + // 0 chars + ensureEncryptDecryptIsIdentity(dict, fileId1, "user", ""); // 4 chars ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaa"); // 5 chars @@ -842,5 +870,61 @@ describe("CipherTransformFactory", function () { "aaaaaaaaaaaaaaaaaaaaaa" ); }); + it("should encrypt and have the correct length using AES128", function () { + dict3.CF = buildDict({ + Identity: buildDict({ + CFM: Name.get("AESV2"), + }), + }); + const dict = buildDict(dict3); + // 0 char + ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", ""); + // 1 char + ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "a"); + // 2 chars + ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "aa"); + // 16 chars + ensureAESEncryptedStringHasCorrectLength( + dict, + fileId1, + "user", + "aaaaaaaaaaaaaaaa" + ); + // 19 chars + ensureAESEncryptedStringHasCorrectLength( + dict, + fileId1, + "user", + "aaaaaaaaaaaaaaaaaaa" + ); + }); + it("should encrypt and have the correct length using AES256", function () { + dict3.CF = buildDict({ + Identity: buildDict({ + CFM: Name.get("AESV3"), + }), + }); + const dict = buildDict(dict3); + // 0 char + ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", ""); + // 4 chars + ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "aaaa"); + // 5 chars + ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "aaaaa"); + // 16 chars + ensureAESEncryptedStringHasCorrectLength( + dict, + fileId1, + "user", + "aaaaaaaaaaaaaaaa" + ); + // 22 chars + ensureAESEncryptedStringHasCorrectLength( + dict, + fileId1, + "user", + "aaaaaaaaaaaaaaaaaaaaaa" + ); + }); }); });