Skip to content

feat(pdf): tell locked PDFs apart from damaged ones - #27

Merged
lars-1987 merged 1 commit into
mainfrom
feat/locked-pdf-detection
Nov 8, 2026
Merged

lars-1987 merged 1 commit into
mainfrom
feat/locked-pdf-detection

Conversation

@lars-1987

@lars-1987 lars-1987 commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

An encrypted PDF used to be reported as damaged. Now it's reported as locked, the card says how it's locked, and for either kind of lock the card links SanctumPDF's in-browser unlock tool. A lock MetaStrip can't classify gets no link.

 stripPdf
-  PDFDocument.load(bytes)              // throws EncryptedPDFError
-    catch → Unreadable                  // "may be damaged", reason "unreadable: pdf"
+  PDFDocument.load(bytes, { ignoreEncryption: true })   // look, never edit
+  if pdfDoc.isEncrypted
+    opensWithoutPassword(pdfDoc)        // standard handler check, empty password
+      true      → Locked("permissions")
+      false     → Locked("password")
+      undefined → Locked("unknown")     // certificates, unknown revision
 <CardReview> failed file
-  "This file could not be read."
+  "This file is locked."               // when reason is locked:*
+  <a SanctumPDF unlock>                // password: "Know the password?"; permissions: "leaving the password blank"
   email link                           // unchanged; locked files never get it

pdf-lock.ts implements ISO 32000 algorithms 2, 4 and 5 (RC4/MD5, revisions 2 to 4) and 2.B (AES-256, revisions 5 and 6, using Web Crypto). Nothing is decrypted: it reads only /ID and /Encrypt, which the standard leaves in the clear.

Telemetry: for these files, file_failed reasons change from unreadable: pdf to locked: pdf password|permissions|unknown. The new unlock_link_clicked {lock} event counts clicks on the link.

Evidence

  • Before: every encrypted PDF gave unreadable: pdf with the message "It may be damaged…".
    After: 341 tests pass (15 new). The new tests use 13 real encrypted PDFs made with qpdf 12.4: revisions 2, 3, 4 and 6, with and without object streams, plus R4 with clear metadata. They also cover a non-standard handler. Each test fails on main.
    r3-open opens without a password, so it's locked by permissions   ✓
    r6-pass-objstm needs a password to open                            ✓
    Adobe.PubSec handler → locked: pdf unknown                         ✓
    locked is never "unexplained" (no email link)                      ✓
    
  • Built site, headless Chrome, PostHog posts answered locally (nothing sent):
    • r6-pass at 1440 px: the card shows "This file is locked." with the SanctumPDF link. file_failed reason is locked: pdf password, and clicking the link sends unlock_link_clicked lock=password.
    • r4-open-objstm: the card shows the permissions copy and links SanctumPDF "leaving the password blank"; the reason is locked: pdf permissions. It reads the same at 390 px.
    • r3-pass at 390 px: the link shows, with no horizontal overflow.
    • Email link on a truncated PDF: unchanged. It still fires failure_report_clicked.
  • Link target: https://sanctumpdf.com/unlock-pdf?utm_source=metastrip&utm_medium=referral&utm_campaign=locked_pdf returns 200.

Merge Danger

Merge order: merge lars-1987/sanctumpdf#1 first and let it deploy. Until it's live, SanctumPDF still disables its button with a blank password, so the link for editing-locked PDFs would lead to a dead end.

Door: two-way

Blast Radius: PDFs

Only encrypted PDFs take the new path; every other PDF loads exactly as before, since ignoreEncryption changes nothing for an unencrypted file. If the check itself throws, the file is still reported as locked (unknown), never as a crash. Weekly reviews must count locked: pdf% alongside unreadable: pdf from now on.

pdf-lib refuses to load an encrypted PDF, and stripPdf caught that with every
other load failure, so a locked file was called damaged ("unreadable: pdf").
Two real ones in the week of 1 Nov 2026 may have been this.

stripPdf now loads with ignoreEncryption (only to look, never to edit) and
throws Locked. The standard security handler's own check, run with an empty
password, says whether the file opens freely (an owner password guards edits)
or needs a password to open. Nothing is decrypted: only /ID and /Encrypt are
read, and the standard leaves those in the clear.

The card says the file is locked instead of unreadable. When it needs a
password, it links SanctumPDF's unlock tool, which takes the password and
also runs in the browser. Permission-only locks get no link, because that
tool needs a password and those files have none to give.

Telemetry: "locked: pdf password|permissions|unknown" replaces
"unreadable: pdf" for these files, and a new unlock_link_clicked event
counts link clicks.

@lars-1987 lars-1987 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

looks good

@lars-1987
lars-1987 merged commit 44397a5 into main Nov 8, 2026
1 check passed
@lars-1987
lars-1987 deleted the feat/locked-pdf-detection branch November 8, 2026 02:24
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.

1 participant