Skip to content

Commit 0d430b7

Browse files
committed
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
1 parent e5922f2 commit 0d430b7

File tree

4 files changed

+71
-16
lines changed

4 files changed

+71
-16
lines changed

src/core/xref.js

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from "./core_utils.js";
3232
import { BaseStream } from "./base_stream.js";
3333
import { CipherTransformFactory } from "./crypto.js";
34+
import { NameTree } from "./name_number_tree.js";
3435

3536
class XRef {
3637
#firstXRefStmPos = null;
@@ -118,22 +119,6 @@ class XRef {
118119
}
119120
warn(`XRef.parse - Invalid "Encrypt" reference: "${ex}".`);
120121
}
121-
if (encrypt instanceof Dict) {
122-
const ids = trailerDict.get("ID");
123-
const fileId = ids?.length ? ids[0] : "";
124-
// The 'Encrypt' dictionary itself should not be encrypted, and by
125-
// setting `suppressEncryption` we can prevent an infinite loop inside
126-
// of `XRef_fetchUncompressed` if the dictionary contains indirect
127-
// objects (fixes issue7665.pdf).
128-
encrypt.suppressEncryption = true;
129-
this.encrypt = new CipherTransformFactory(
130-
encrypt,
131-
fileId,
132-
this.pdfManager.password
133-
);
134-
}
135-
136-
// Get the root dictionary (catalog) object, and do some basic validation.
137122
let root;
138123
try {
139124
root = trailerDict.get("Root");
@@ -143,6 +128,52 @@ class XRef {
143128
}
144129
warn(`XRef.parse - Invalid "Root" reference: "${ex}".`);
145130
}
131+
132+
if (encrypt instanceof Dict) {
133+
const ids = trailerDict.get("ID");
134+
const fileId = ids?.length ? ids[0] : "";
135+
136+
const stmF = encrypt.get("StmF")?.name ?? "Identity";
137+
const strF = encrypt.get("StrF")?.name ?? "Identity";
138+
const eff = encrypt.has("EFF") ? encrypt.get("EFF")?.name : strF;
139+
const cryptFilters = encrypt.get("CF") || Dict.empty;
140+
// Check if only the file attachments are encrypted.
141+
if (
142+
stmF === "Identity" &&
143+
strF === "Identity" &&
144+
eff !== "Identity" &&
145+
cryptFilters.has(eff) &&
146+
cryptFilters.get(eff)?.get("CFM") !== "None"
147+
) {
148+
let hasEncryptedAttachments = false;
149+
if (root instanceof Dict) {
150+
const names = root.get("Names");
151+
if (names instanceof Dict && names.has("EmbeddedFiles")) {
152+
const nameTree = new NameTree(names.getRaw("EmbeddedFiles"), this);
153+
const attachments = nameTree.getAll();
154+
if (attachments.size > 0) {
155+
hasEncryptedAttachments = true;
156+
}
157+
}
158+
}
159+
if (!hasEncryptedAttachments) {
160+
// If there are no encrypted attachments, encrypt dictionary is
161+
// not needed.
162+
encrypt = null;
163+
}
164+
} else {
165+
// The 'Encrypt' dictionary itself should not be encrypted, and by
166+
// setting `suppressEncryption` we can prevent an infinite loop inside
167+
// of `XRef_fetchUncompressed` if the dictionary contains indirect
168+
// objects (fixes issue7665.pdf).
169+
encrypt.suppressEncryption = true;
170+
this.encrypt = new CipherTransformFactory(
171+
encrypt,
172+
fileId,
173+
this.pdfManager.password
174+
);
175+
}
176+
}
146177
if (root instanceof Dict) {
147178
try {
148179
const pages = root.get("Pages");

test/pdfs/issue20049.pdf.link

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
https://github.com/user-attachments/files/20997912/encryption.pdf

test/test_manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8751,6 +8751,14 @@
87518751
}
87528752
}
87538753
},
8754+
{
8755+
"id": "issue20049",
8756+
"file": "pdfs/issue20049.pdf",
8757+
"md5": "1cdfde56be6b070e0c18aafc487d92ff",
8758+
"rounds": 1,
8759+
"link": true,
8760+
"type": "eq"
8761+
},
87548762
{
87558763
"id": "bug1778692",
87568764
"file": "pdfs/bug1778692.pdf",

test/unit/api_spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,21 @@ describe("api", function () {
878878

879879
await loadingTask.destroy();
880880
});
881+
882+
it("should not prompt for password if only attachments are encrypted and there are none", async function () {
883+
const loadingTask = getDocument(buildGetDocumentParams("issue20049.pdf"));
884+
expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true);
885+
886+
loadingTask.onPassword = function (callback, reason) {
887+
if (reason === PasswordResponses.NEED_PASSWORD) {
888+
expect(false).toEqual(true);
889+
throw new Error("Should not prompt for password.");
890+
}
891+
};
892+
893+
const pdfDocument = await loadingTask.promise;
894+
expect(pdfDocument.numPages).toBeGreaterThan(0);
895+
});
881896
});
882897

883898
describe("PDFWorker", function () {

0 commit comments

Comments
 (0)