Skip to content

Commit 64ecd4c

Browse files
committed
Add support for encrypted attachments
This PR is related to GH-20732, which is about `AuthEvent` (to delay promting for a password), but instead adds the actual support for encrypted attachments. “Encrypted attachments” means that the main things are plain text. Note that some PDF viewers, like Preview/QuickLook/Safari or Chrome, do not support attachments at all. Note that the file checked into the tests is the same as `output-no-auth-event.pdf` referenced in <#20139 (comment)>. Closes GH-20139.
1 parent bf9ae76 commit 64ecd4c

5 files changed

Lines changed: 247 additions & 80 deletions

File tree

src/core/crypto.js

Lines changed: 115 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ import { calculateMD5 } from "./calculate_md5.js";
3131
import { calculateSHA256 } from "./calculate_sha256.js";
3232
import { DecryptStream } from "./decrypt_stream.js";
3333

34+
/**
35+
* @typedef {typeof AES128Cipher | typeof AES256Cipher | typeof ARCFourCipher
36+
* | typeof NullCipher} CipherConstructors
37+
*/
38+
39+
/**
40+
* @callback ResolveCipher
41+
* Find the appropriate cipher class based on the filter name.
42+
* @param {Name | null} [filterName]
43+
* Name.
44+
* @returns {CipherConstructors}
45+
* Cipher constructor.
46+
*/
47+
3448
class ARCFourCipher {
3549
a = 0;
3650

@@ -737,13 +751,48 @@ class PDF20 extends PDFBase {
737751
}
738752

739753
class CipherTransform {
740-
constructor(stringCipherConstructor, streamCipherConstructor) {
741-
this.StringCipherConstructor = stringCipherConstructor;
742-
this.StreamCipherConstructor = streamCipherConstructor;
754+
/**
755+
* @param {ResolveCipher} resolveCipher
756+
* Resolve a cipher constructor from a crypt filter name.
757+
* @param {Name | null} [stringFilterName]
758+
* Default crypt filter for strings.
759+
* @param {Name | null} [streamFilterName]
760+
* Default crypt filter for streams.
761+
*/
762+
constructor(resolveCipher, stringFilterName = null, streamFilterName = null) {
763+
/** @type {Map<string, CipherConstructors>} */
764+
this.cipherCache = new Map();
765+
this.resolveCipher = resolveCipher;
766+
this.streamFilterName = streamFilterName;
767+
this.stringFilterName = stringFilterName;
743768
}
744769

745-
createStream(stream, length) {
746-
const cipher = new this.StreamCipherConstructor();
770+
/**
771+
* @param {Name | null} [filterName]
772+
* Crypt filter name.
773+
* @returns {CipherConstructors}
774+
* Cipher constructor.
775+
*/
776+
#getCipher(filterName = null) {
777+
const key = filterName ? filterName.name : "__default__";
778+
779+
let Cipher = this.cipherCache.get(key);
780+
if (!Cipher) {
781+
Cipher = this.resolveCipher(filterName);
782+
this.cipherCache.set(key, Cipher);
783+
}
784+
return Cipher;
785+
}
786+
787+
/**
788+
* @param {BaseStream} stream
789+
* @param {number | null} length
790+
* @param {Name | null} [cryptFilterName]
791+
* @returns {DecryptStream}
792+
*/
793+
createStream(stream, length, cryptFilterName = null) {
794+
const Cipher = this.#getCipher(cryptFilterName || this.streamFilterName);
795+
const cipher = new Cipher();
747796
return new DecryptStream(
748797
stream,
749798
length,
@@ -754,14 +803,16 @@ class CipherTransform {
754803
}
755804

756805
decryptString(s) {
757-
const cipher = new this.StringCipherConstructor();
806+
const Cipher = this.#getCipher(this.stringFilterName);
807+
const cipher = new Cipher();
758808
let data = stringToBytes(s);
759809
data = cipher.decryptBlock(data, true);
760810
return bytesToString(data);
761811
}
762812

763813
encryptString(s) {
764-
const cipher = new this.StringCipherConstructor();
814+
const Cipher = this.#getCipher(this.stringFilterName);
815+
const cipher = new Cipher();
765816
if (cipher instanceof AESBaseCipher) {
766817
// Append some chars equal to "16 - (M mod 16)"
767818
// where M is the string length (see section 7.6.2 in PDF specification)
@@ -986,41 +1037,6 @@ class CipherTransformFactory {
9861037
return hash.subarray(0, Math.min(n + 5, 16));
9871038
}
9881039

989-
#buildCipherConstructor(cf, name, num, gen, key) {
990-
if (!(name instanceof Name)) {
991-
throw new FormatError("Invalid crypt filter name.");
992-
}
993-
const self = this;
994-
const cryptFilter = cf.get(name.name);
995-
const cfm = cryptFilter?.get("CFM");
996-
997-
if (!cfm || cfm.name === "None") {
998-
return function () {
999-
return new NullCipher();
1000-
};
1001-
}
1002-
if (cfm.name === "V2") {
1003-
return function () {
1004-
return new ARCFourCipher(
1005-
self.#buildObjectKey(num, gen, key, /* isAes = */ false)
1006-
);
1007-
};
1008-
}
1009-
if (cfm.name === "AESV2") {
1010-
return function () {
1011-
return new AES128Cipher(
1012-
self.#buildObjectKey(num, gen, key, /* isAes = */ true)
1013-
);
1014-
};
1015-
}
1016-
if (cfm.name === "AESV3") {
1017-
return function () {
1018-
return new AES256Cipher(key);
1019-
};
1020-
}
1021-
throw new FormatError("Unknown crypto method");
1022-
}
1023-
10241040
constructor(dict, fileId, password) {
10251041
const filter = dict.get("Filter");
10261042
if (!isName(filter, "Standard")) {
@@ -1185,43 +1201,74 @@ class CipherTransformFactory {
11851201
}
11861202
}
11871203

1204+
/**
1205+
* @param {number} num
1206+
* Object number.
1207+
* @param {number} gen
1208+
* Generation number.
1209+
* @returns {CipherTransform}
1210+
* Cipher transform.
1211+
*/
11881212
createCipherTransform(num, gen) {
11891213
if (this.algorithm === 4 || this.algorithm === 5) {
1190-
return new CipherTransform(
1191-
this.#buildCipherConstructor(
1192-
this.cf,
1193-
this.strf,
1194-
num,
1195-
gen,
1196-
this.encryptionKey
1197-
),
1198-
this.#buildCipherConstructor(
1199-
this.cf,
1200-
this.stmf,
1201-
num,
1202-
gen,
1203-
this.encryptionKey
1204-
)
1205-
);
1214+
/** @type {ResolveCipher} */
1215+
const resolveCipher = filterName => {
1216+
if (!(filterName instanceof Name)) {
1217+
throw new FormatError("Invalid crypt filter name.");
1218+
}
1219+
const cryptFilter = this.cf.get(filterName.name);
1220+
const cfm = cryptFilter?.get("CFM");
1221+
1222+
if (!cfm || cfm.name === "None") {
1223+
return NullCipher;
1224+
}
1225+
if (cfm.name === "V2") {
1226+
return ARCFourCipher.bind(
1227+
null,
1228+
this.#buildObjectKey(
1229+
num,
1230+
gen,
1231+
this.encryptionKey,
1232+
/* isAes = */ false
1233+
)
1234+
);
1235+
}
1236+
if (cfm.name === "AESV2") {
1237+
return AES128Cipher.bind(
1238+
null,
1239+
this.#buildObjectKey(
1240+
num,
1241+
gen,
1242+
this.encryptionKey,
1243+
/* isAes = */ true
1244+
)
1245+
);
1246+
}
1247+
if (cfm.name === "AESV3") {
1248+
return AES256Cipher.bind(null, this.encryptionKey);
1249+
}
1250+
throw new FormatError("Unknown crypto method");
1251+
};
1252+
1253+
return new CipherTransform(resolveCipher, this.strf, this.stmf);
12061254
}
1255+
12071256
// algorithms 1 and 2
1208-
const key = this.#buildObjectKey(
1209-
num,
1210-
gen,
1211-
this.encryptionKey,
1212-
/* isAes = */ false
1213-
);
1214-
const cipherConstructor = function () {
1215-
return new ARCFourCipher(key);
1216-
};
1217-
return new CipherTransform(cipherConstructor, cipherConstructor);
1257+
/** @type {ResolveCipher} */
1258+
const resolveCipher = () =>
1259+
ARCFourCipher.bind(
1260+
null,
1261+
this.#buildObjectKey(num, gen, this.encryptionKey, /* isAes = */ false)
1262+
);
1263+
return new CipherTransform(resolveCipher);
12181264
}
12191265
}
12201266

12211267
export {
12221268
AES128Cipher,
12231269
AES256Cipher,
12241270
ARCFourCipher,
1271+
CipherTransform,
12251272
CipherTransformFactory,
12261273
PDF17,
12271274
PDF20,

0 commit comments

Comments
 (0)