Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/core/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment on lines +1063 to +1064
Copy link
Contributor

@nicolo-ribaudo nicolo-ribaudo Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we console.warn something like "there are encrypted attachments but they are not supported yet"?

Also, question about the PDF format: is it possible to have some encrypted attachments and some not? In this case we should probably still show the non-encrypted ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to have some encrypted attachments and some not? In this case we should probably still show the non-encrypted ones.

See the last 2 blockquotes from #20139 (comment).
As I understand it, pdf generators should make PDFs with all attachments encrypted.

There is also the case of encrypted attachments w/o AuthEvent.
I considered looking into it, indeed having a nice warning, but I think it’s better to spend that time into actually solving the issue #20139.

So this also relates to console.warn: this PR does not change encrypted attachment handling. Those are not detected and yield warnings already. This only honors when to ask for a password.

!this.xref.decryptOnAttachmentOpen
) {
const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref);
for (const [key, value] of nameTree.getAll()) {
const fs = new FileSpec(value);
Expand Down
33 changes: 21 additions & 12 deletions src/core/xref.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class XRef {
this._newPersistentRefNum = null;
this._newTemporaryRefNum = null;
this._persistentRefsCache = null;
this.decryptOnAttachmentOpen = false;
}

getNewPersistentRef(obj) {
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@
!bug1020226.pdf
!issue9534_reduced.pdf
!attachment.pdf
!issue20049.pdf
!basicapi.pdf
!issue15590.pdf
!issue15594_reduced.pdf
Expand Down
Binary file added test/pdfs/issue20049.pdf
Binary file not shown.
7 changes: 7 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down