From 995f6f2dde88dce365cdae8f7657505a3c41841e Mon Sep 17 00:00:00 2001 From: Aditi Date: Fri, 1 Aug 2025 14:04:25 +0530 Subject: [PATCH] Avoid password prompt on attachment-only encryption when there are no attachments PDF.js wrongly prompted for passwords when a PDF had no attachments but had an encryption enabled that was limited to only attachments leaving all page, image and metadata streams in clear text. The encryption dictionary is now discarded for this case. This prevents unnecessary prompts for PDFs with no protected content beyond attachments --- src/core/xref.js | 54 +++++++++++++++++++++++++++------------ test/pdfs/.gitignore | 1 + test/pdfs/issue20049.pdf | Bin 0 -> 11722 bytes test/test_manifest.json | 7 +++++ test/unit/api_spec.js | 15 +++++++++++ 5 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 test/pdfs/issue20049.pdf diff --git a/src/core/xref.js b/src/core/xref.js index da7491075b709..af185c6057979 100644 --- a/src/core/xref.js +++ b/src/core/xref.js @@ -31,6 +31,7 @@ import { } from "./core_utils.js"; import { BaseStream } from "./base_stream.js"; import { CipherTransformFactory } from "./crypto.js"; +import { NameTree } from "./name_number_tree.js"; class XRef { #firstXRefStmPos = null; @@ -118,22 +119,6 @@ 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 - ); - } - - // Get the root dictionary (catalog) object, and do some basic validation. let root; try { root = trailerDict.get("Root"); @@ -143,6 +128,43 @@ class XRef { } warn(`XRef.parse - Invalid "Root" reference: "${ex}".`); } + + if (encrypt instanceof Dict) { + // Check if only the file attachments are encrypted. + if ( + encrypt.get("CF")?.get("StdCF")?.get("AuthEvent")?.name === "EFOpen" + ) { + let hasEncryptedAttachments = false; + if (root instanceof Dict) { + const names = root.get("Names"); + if (names instanceof Dict && names.has("EmbeddedFiles")) { + const nameTree = new NameTree(names.getRaw("EmbeddedFiles"), this); + const attachments = nameTree.getAll(); + if (attachments.size > 0) { + hasEncryptedAttachments = true; + } + } + } + if (!hasEncryptedAttachments) { + // If there are no encrypted attachments, encrypt dictionary is + // not needed. + encrypt = null; + } + } else { + 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 + ); + } + } if (root instanceof Dict) { try { const pages = root.get("Pages"); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 233649a1f1df1..582e32a584fc3 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 0000000000000000000000000000000000000000..ec0ed82e0f42152781823759c7ca4c1a59be8698 GIT binary patch literal 11722 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 VLm%p6e+TJT=`R>8`Mvn}e*lAoE4Bat literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index 2e28f992fc89a..8c3b37bb6aea0 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -4930,6 +4930,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 e5fdf5d3003a3..52714028b8abe 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -878,6 +878,21 @@ describe("api", function () { await loadingTask.destroy(); }); + + it("should not prompt for password if only attachments are encrypted and there are none", async function () { + const loadingTask = getDocument(buildGetDocumentParams("issue20049.pdf")); + expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true); + + loadingTask.onPassword = function (callback, reason) { + if (reason === PasswordResponses.NEED_PASSWORD) { + expect(false).toEqual(true); + throw new Error("Should not prompt for password."); + } + }; + + const pdfDocument = await loadingTask.promise; + expect(pdfDocument.numPages).toBeGreaterThan(0); + }); }); describe("PDFWorker", function () {