Skip to content

Commit 9619bf9

Browse files
committed
Correctly pad strings when saving an encrypted pdf (bug 1726789)
1 parent 2ed133b commit 9619bf9

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

src/core/crypto.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1407,11 +1407,12 @@ class CipherTransform {
14071407
// Append some chars equal to "16 - (M mod 16)"
14081408
// where M is the string length (see section 7.6.2 in PDF specification)
14091409
// to have a final string where the length is a multiple of 16.
1410+
// Special note:
1411+
// "Note that the pad is present when M is evenly divisible by 16;
1412+
// it contains 16 bytes of 0x10."
14101413
const strLen = s.length;
14111414
const pad = 16 - (strLen % 16);
1412-
if (pad !== 16) {
1413-
s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad));
1414-
}
1415+
s += String.fromCharCode(pad).repeat(pad);
14151416

14161417
// Generate an initialization vector
14171418
const iv = new Uint8Array(16);

test/unit/crypto_spec.js

+84
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,30 @@ describe("CipherTransformFactory", function () {
581581
}
582582
}
583583

584+
function ensureAESEncryptedStringHasCorrectLength(
585+
dict,
586+
fileId,
587+
password,
588+
string
589+
) {
590+
const factory = new CipherTransformFactory(dict, fileId, password);
591+
const cipher = factory.createCipherTransform(123, 0);
592+
const encrypted = cipher.encryptString(string);
593+
594+
// The final length is a multiple of 16.
595+
// If the initial string has a length which is a multiple of 16
596+
// then 16 chars of padding are added.
597+
// So we've the mapping:
598+
// - length: [0-15] => new length: 16
599+
// - length: [16-31] => new length: 32
600+
// - length: [32-47] => new length: 48
601+
// ...
602+
expect(encrypted.length).toEqual(
603+
16 /* initialization vector length */ +
604+
16 * Math.ceil((string.length + 1) / 16)
605+
);
606+
}
607+
584608
function ensureEncryptDecryptIsIdentity(dict, fileId, password, string) {
585609
const factory = new CipherTransformFactory(dict, fileId, password);
586610
const cipher = factory.createCipherTransform(123, 0);
@@ -807,6 +831,8 @@ describe("CipherTransformFactory", function () {
807831
}),
808832
});
809833
const dict = buildDict(dict3);
834+
// 0 char
835+
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "");
810836
// 1 char
811837
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "a");
812838
// 2 chars
@@ -828,6 +854,8 @@ describe("CipherTransformFactory", function () {
828854
}),
829855
});
830856
const dict = buildDict(dict3);
857+
// 0 chars
858+
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "");
831859
// 4 chars
832860
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaa");
833861
// 5 chars
@@ -842,5 +870,61 @@ describe("CipherTransformFactory", function () {
842870
"aaaaaaaaaaaaaaaaaaaaaa"
843871
);
844872
});
873+
it("should encrypt and have the correct length using AES128", function () {
874+
dict3.CF = buildDict({
875+
Identity: buildDict({
876+
CFM: Name.get("AESV2"),
877+
}),
878+
});
879+
const dict = buildDict(dict3);
880+
// 0 char
881+
ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "");
882+
// 1 char
883+
ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "a");
884+
// 2 chars
885+
ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "aa");
886+
// 16 chars
887+
ensureAESEncryptedStringHasCorrectLength(
888+
dict,
889+
fileId1,
890+
"user",
891+
"aaaaaaaaaaaaaaaa"
892+
);
893+
// 19 chars
894+
ensureAESEncryptedStringHasCorrectLength(
895+
dict,
896+
fileId1,
897+
"user",
898+
"aaaaaaaaaaaaaaaaaaa"
899+
);
900+
});
901+
it("should encrypt and have the correct length using AES256", function () {
902+
dict3.CF = buildDict({
903+
Identity: buildDict({
904+
CFM: Name.get("AESV3"),
905+
}),
906+
});
907+
const dict = buildDict(dict3);
908+
// 0 char
909+
ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "");
910+
// 4 chars
911+
ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "aaaa");
912+
// 5 chars
913+
ensureAESEncryptedStringHasCorrectLength(dict, fileId1, "user", "aaaaa");
914+
// 16 chars
915+
ensureAESEncryptedStringHasCorrectLength(
916+
dict,
917+
fileId1,
918+
"user",
919+
"aaaaaaaaaaaaaaaa"
920+
);
921+
// 22 chars
922+
ensureAESEncryptedStringHasCorrectLength(
923+
dict,
924+
fileId1,
925+
"user",
926+
"aaaaaaaaaaaaaaaaaaaaaa"
927+
);
928+
});
845929
});
846930
});

0 commit comments

Comments
 (0)