Skip to content

fix(pdfkit): encrypt strings inside name trees - #3508

Closed
klimeryk wants to merge 1 commit into
diegomura:masterfrom
klimeryk:master
Closed

fix(pdfkit): encrypt strings inside name trees#3508
klimeryk wants to merge 1 commit into
diegomura:masterfrom
klimeryk:master

Conversation

@klimeryk

Copy link
Copy Markdown

Since encryption landed in #3344, setting userPassword on a <Document> silently breaks two things:

  • internal links. <Link src="#chapter-2"> to a node with id="chapter-2" no longer navigates. Firefox's viewer logs goToDestination: "null" is not a valid destination array, for dest="chapter-2".
  • attachments. ctx.file() embeds the file, but its name comes back as garbage, so it cannot be looked up.

Both are the same bug, inherited from upstream pdfkit (foliojs/pdfkit#1513). I've ran into this while trying to add password support to my own project that uses react-pdf: klimeryk/recalendar.js@f62f765.

I've submitted this patch and it got merged upstream already: foliojs/pdfkit#1773 🙇

Cause

Strings inside name trees are written unencrypted in an otherwise encrypted document. Readers decrypt them like every other string, get garbage, and the lookup fails.

PDFObject.convert() discards the cipher for every PDFTree, so PDFTree.toString() has nothing to pass on when it serialises /Limits, the keys and the values — and name tree keys are PDF strings.

In an encrypted document, the strings inside name trees are written unencrypted. Readers decrypt them like every other string, get garbage, and the lookup fails. Anything that relies on a name tree is broken:

  • internal links to named destinations go nowhere
  • attachments cannot be found by name
  • named JavaScript never runs - and both its name and its body leak in plain text out of an encrypted document

Reproduction

import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs';

const doc = new PDFDocument({ userPassword: 'secret' });
const chunks = [];
doc.on('data', (c) => chunks.push(c));
doc.on('end', async () => {
  const pdf = await getDocument({ data: Buffer.concat(chunks), password: 'secret' }).promise;
  console.log(await pdf.getDestination('heading'));
  console.log(Object.keys(await pdf.getAttachments()));
});

doc.text('go', { destination: 'heading' });
doc.file(new Uint8Array([1, 2, 3]), { name: 'data.txt' });
doc.end();
destination attachment
before null [ 'v*\x8E(äì\x91ı' ]
after [ { num: 12, gen: 0 }, { name: 'XYZ' }, 72, 720, null ] [ 'data.txt' ]

Without a password both cases already printed the correct values.

Tests

packages/pdfkit/tests/tree.test.ts:

  • a tree is serialised in plain text without a cipher (regression guard) and fully encrypted with one, parametrized over PDFNameTree and PDFNumberTree (whose keys are integers, so only its values are encrypted)
  • a document with a named destination, an attachment and a named script contains (heading), (data.txt) and app.alert when unencrypted, and none of them when encrypted — checked across RC4, AES-128 and AES-256

The eleven encryption cases fail on master and pass with this change.

In an encrypted document, the strings inside name trees were being written
unencrypted. PDF readers would then try to decrypt them like every
other string, get garbage, and the lookup would fail.

Based on fix submitted and merged upstream:
foliojs/pdfkit#1773
@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: be261c9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@react-pdf/pdfkit Patch
@react-pdf/font Patch
@react-pdf/renderer Patch
@react-pdf/types Patch
@react-pdf/math Patch
@react-pdf/mermaid Patch
next-14 Patch
next-15 Patch
@react-pdf/vite-example Patch
@react-pdf/layout Patch
@react-pdf/render Patch
@react-pdf/stylesheet Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@diegomura

Copy link
Copy Markdown
Owner

Hey @klimeryk ! This is an awful timing but I was just working on removing our pdfkit in #3509. This is something I wanted for months and that we can finally do, but that means this fix needs to land there. It's good though, so everyone besides react-pdf can benefit from it

@diegomura diegomura closed this Aug 23, 2026
@klimeryk

klimeryk commented Aug 23, 2026

Copy link
Copy Markdown
Author

Hahahaha, indeed bad timing! 😆 But that's super good news overall, congrats on that big milestone, I know it's been in the works for a while 🎉 And this fix has been already merged upstream (foliojs/pdfkit#1773) , so once a new version is released (0.20.2?) then this fix will be pulled in. and looks like it's already part of 0.20.0 and as a result it's also part of @react-pdf 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants