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
This commit is contained in:
Aditi 2025-08-01 14:04:25 +05:30
parent e5922f2e72
commit 995f6f2dde
5 changed files with 61 additions and 16 deletions

View File

@ -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");

View File

@ -385,6 +385,7 @@
!bug1020226.pdf
!issue9534_reduced.pdf
!attachment.pdf
!issue20049.pdf
!basicapi.pdf
!issue15590.pdf
!issue15594_reduced.pdf

BIN
test/pdfs/issue20049.pdf Normal file

Binary file not shown.

View File

@ -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",

View File

@ -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 () {