Skip to content

Commit e92ce0c

Browse files
committed
Add support for AuthEvent
This solves GH-20049, it’s an alternative to GH-20140. The analysis is a bit different. It builds on [my comment in the other issue](#20139 (comment)), and the fact that they are *separate* things. To recap, it is possible to have a document plain text but have attachments encrypted. In that case, instead of prompting upfront, PDFs can prompt later with `/AuthEvent /EFOpen`. The default is `/AuthEvent /DocOpen`. Which is typical. So `/AuthEvent` is uncommon. So, separate things: * encrypted attachments (regardless of `/AuthEvent`) (GH-20139) * `/AuthEvent /EFOpen` (regardless of whether there are attachments) (this issue/PR) This PR stops prompting for a password on doc open if there is an `/AuthEvent /EFOpen`. It also does not list delayed encrypted attachments in the sidebar. It does that to prevent an infinite loading screen in a known case but also so that there is a place marked in the code where future logic, after GH-20139, can support lazily decrypting attachments.
1 parent 909a700 commit e92ce0c

6 files changed

Lines changed: 53 additions & 13 deletions

File tree

src/core/catalog.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,16 @@ class Catalog {
10541054
const obj = this.#catDict.get("Names");
10551055
let attachments = null;
10561056

1057-
if (obj instanceof Dict && obj.has("EmbeddedFiles")) {
1057+
if (
1058+
obj instanceof Dict &&
1059+
obj.has("EmbeddedFiles") &&
1060+
// Note: decrypting attachments is not supported regardless.
1061+
// If it was, then `decryptOnAttachmentOpen` would signal whether to do
1062+
// so lazily.
1063+
// As it stands, we can at least avoid users getting to an infinite loader
1064+
// in the case of `decryptOnAttachmentOpen`.
1065+
!this.xref.decryptOnAttachmentOpen
1066+
) {
10581067
const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref);
10591068
for (const [key, value] of nameTree.getAll()) {
10601069
const fs = new FileSpec(value);

src/core/xref.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class XRef {
4343
this._newPersistentRefNum = null;
4444
this._newTemporaryRefNum = null;
4545
this._persistentRefsCache = null;
46+
this.decryptOnAttachmentOpen = false;
4647
}
4748

4849
getNewPersistentRef(obj) {
@@ -117,18 +118,26 @@ class XRef {
117118
warn(`XRef.parse - Invalid "Encrypt" reference: "${ex}".`);
118119
}
119120
if (encrypt instanceof Dict) {
120-
const ids = trailerDict.get("ID");
121-
const fileId = ids?.length ? ids[0] : "";
122-
// The 'Encrypt' dictionary itself should not be encrypted, and by
123-
// setting `suppressEncryption` we can prevent an infinite loop inside
124-
// of `XRef_fetchUncompressed` if the dictionary contains indirect
125-
// objects (fixes issue7665.pdf).
126-
encrypt.suppressEncryption = true;
127-
this.encrypt = new CipherTransformFactory(
128-
encrypt,
129-
fileId,
130-
this.pdfManager.password
131-
);
121+
// Note: decrypting attachments is not supported regardless.
122+
// But it is at least possible to honour `/AuthEvent /EFOpen` by not asking
123+
// for a password on document open.
124+
this.decryptOnAttachmentOpen =
125+
encrypt.get("CF")?.get("StdCF")?.get("AuthEvent")?.name === "EFOpen";
126+
127+
if (!this.decryptOnAttachmentOpen) {
128+
const ids = trailerDict.get("ID");
129+
const fileId = ids?.length ? ids[0] : "";
130+
// The 'Encrypt' dictionary itself should not be encrypted, and by
131+
// setting `suppressEncryption` we can prevent an infinite loop inside
132+
// of `XRef_fetchUncompressed` if the dictionary contains indirect
133+
// objects (fixes issue7665.pdf).
134+
encrypt.suppressEncryption = true;
135+
this.encrypt = new CipherTransformFactory(
136+
encrypt,
137+
fileId,
138+
this.pdfManager.password
139+
);
140+
}
132141
}
133142

134143
// Get the root dictionary (catalog) object, and do some basic validation.

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@
385385
!bug1020226.pdf
386386
!issue9534_reduced.pdf
387387
!attachment.pdf
388+
!issue20049.pdf
388389
!basicapi.pdf
389390
!issue15590.pdf
390391
!issue15594_reduced.pdf

test/pdfs/issue20049.pdf

11.4 KB
Binary file not shown.

test/test_manifest.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5524,6 +5524,13 @@
55245524
"rounds": 1,
55255525
"type": "eq"
55265526
},
5527+
{
5528+
"id": "issue20049",
5529+
"file": "pdfs/issue20049.pdf",
5530+
"md5": "1cdfde56be6b070e0c18aafc487d92ff",
5531+
"rounds": 1,
5532+
"type": "eq"
5533+
},
55275534
{
55285535
"id": "issue8117",
55295536
"file": "pdfs/issue8117.pdf",

test/unit/api_spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,20 @@ describe("api", function () {
888888
await loadingTask.destroy();
889889
});
890890

891+
it("should not prompt for password if `/AuthEvent /EFOpen`", async function () {
892+
const loadingTask = getDocument(buildGetDocumentParams("issue20049.pdf"));
893+
expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true);
894+
let called = false;
895+
896+
loadingTask.onPassword = function () {
897+
called = true;
898+
};
899+
900+
const pdfDocument = await loadingTask.promise;
901+
expect(pdfDocument.numPages).toBeGreaterThan(0);
902+
expect(called).toBe(false);
903+
});
904+
891905
it("Doesn't iterate over all empty slots in the xref entries (bug 1980958)", async function () {
892906
if (isNodeJS) {
893907
pending("Worker is not supported in Node.js.");

0 commit comments

Comments
 (0)