Skip to content

Commit 74bf518

Browse files
committed
Avoid password prompt when only encrypted attachments exist
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 74bf518

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

src/core/xref.js

Lines changed: 46 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,51 @@ 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.get(eff)?.get("CFM") !== "None"
146+
) {
147+
let hasEncryptedAttachments = false;
148+
if (root instanceof Dict) {
149+
const names = root.get("Names");
150+
if (names instanceof Dict && names.has("EmbeddedFiles")) {
151+
const nameTree = new NameTree(names.getRaw("EmbeddedFiles"), this);
152+
const attachments = nameTree.getAll();
153+
if (attachments.size > 0) {
154+
hasEncryptedAttachments = true;
155+
}
156+
}
157+
}
158+
if (!hasEncryptedAttachments) {
159+
// If there are no encrypted attachments, encrypt dictionary is
160+
// not needed.
161+
encrypt = null;
162+
}
163+
} else {
164+
// The 'Encrypt' dictionary itself should not be encrypted, and by
165+
// setting `suppressEncryption` we can prevent an infinite loop inside
166+
// of `XRef_fetchUncompressed` if the dictionary contains indirect
167+
// objects (fixes issue7665.pdf).
168+
encrypt.suppressEncryption = true;
169+
this.encrypt = new CipherTransformFactory(
170+
encrypt,
171+
fileId,
172+
this.pdfManager.password
173+
);
174+
}
175+
}
146176
if (root instanceof Dict) {
147177
try {
148178
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,23 @@ describe("api", function () {
878878

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

883900
describe("PDFWorker", function () {

0 commit comments

Comments
 (0)