diff --git a/src/core/catalog.js b/src/core/catalog.js index 9ab10f281a0f5..490a1fe3b54fe 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -1054,7 +1054,16 @@ class Catalog { const obj = this.#catDict.get("Names"); let attachments = null; - if (obj instanceof Dict && obj.has("EmbeddedFiles")) { + if ( + obj instanceof Dict && + obj.has("EmbeddedFiles") && + // Note: decrypting attachments is not supported regardless. + // If it was, then `decryptOnAttachmentOpen` would signal whether to do + // so lazily. + // As it stands, we can at least avoid users getting to an infinite loader + // in the case of `decryptOnAttachmentOpen`. + !this.xref.decryptOnAttachmentOpen + ) { const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref); for (const [key, value] of nameTree.getAll()) { const fs = new FileSpec(value); diff --git a/src/core/xref.js b/src/core/xref.js index 6ff5a1e52e059..7763219b487f8 100644 --- a/src/core/xref.js +++ b/src/core/xref.js @@ -43,6 +43,7 @@ class XRef { this._newPersistentRefNum = null; this._newTemporaryRefNum = null; this._persistentRefsCache = null; + this.decryptOnAttachmentOpen = false; } getNewPersistentRef(obj) { @@ -117,18 +118,26 @@ class XRef { warn(`XRef.parse - Invalid "Encrypt" reference: "${ex}".`); } if (encrypt instanceof Dict) { - const ids = trailerDict.get("ID"); - const fileId = ids?.length ? ids[0] : ""; - // The 'Encrypt' dictionary itself should not be encrypted, and by - // setting `suppressEncryption` we can prevent an infinite loop inside - // of `XRef_fetchUncompressed` if the dictionary contains indirect - // objects (fixes issue7665.pdf). - encrypt.suppressEncryption = true; - this.encrypt = new CipherTransformFactory( - encrypt, - fileId, - this.pdfManager.password - ); + // Note: decrypting attachments is not supported regardless. + // But it is at least possible to honour `/AuthEvent /EFOpen` by not + // asking for a password on document open. + this.decryptOnAttachmentOpen = + encrypt.get("CF")?.get("StdCF")?.get("AuthEvent")?.name === "EFOpen"; + + if (!this.decryptOnAttachmentOpen) { + const ids = trailerDict.get("ID"); + const fileId = ids?.length ? ids[0] : ""; + // The 'Encrypt' dictionary itself should not be encrypted, and by + // setting `suppressEncryption` we can prevent an infinite loop inside + // of `XRef_fetchUncompressed` if the dictionary contains indirect + // objects (fixes issue7665.pdf). + encrypt.suppressEncryption = true; + this.encrypt = new CipherTransformFactory( + encrypt, + fileId, + this.pdfManager.password + ); + } } // Get the root dictionary (catalog) object, and do some basic validation. diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 73665e3be4df2..e2aaf1047e624 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -385,6 +385,7 @@ !bug1020226.pdf !issue9534_reduced.pdf !attachment.pdf +!issue20049.pdf !basicapi.pdf !issue15590.pdf !issue15594_reduced.pdf diff --git a/test/pdfs/issue20049.pdf b/test/pdfs/issue20049.pdf new file mode 100644 index 0000000000000..ec0ed82e0f421 Binary files /dev/null and b/test/pdfs/issue20049.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index f7a8f866df448..f053a4de24e91 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -5524,6 +5524,13 @@ "rounds": 1, "type": "eq" }, + { + "id": "issue20049", + "file": "pdfs/issue20049.pdf", + "md5": "1cdfde56be6b070e0c18aafc487d92ff", + "rounds": 1, + "type": "eq" + }, { "id": "issue8117", "file": "pdfs/issue8117.pdf", diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index e9961a16d7bc9..7d336c6d256b4 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -888,6 +888,20 @@ describe("api", function () { await loadingTask.destroy(); }); + it("should not prompt for password if `/AuthEvent /EFOpen`", async function () { + const loadingTask = getDocument(buildGetDocumentParams("issue20049.pdf")); + expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true); + let called = false; + + loadingTask.onPassword = function () { + called = true; + }; + + const pdfDocument = await loadingTask.promise; + expect(pdfDocument.numPages).toBeGreaterThan(0); + expect(called).toBe(false); + }); + it("Doesn't iterate over all empty slots in the xref entries (bug 1980958)", async function () { if (isNodeJS) { pending("Worker is not supported in Node.js.");