Commit Graph
18 Commits
Author SHA1 Message Date
b642e327e7 Security: fix 10 verified audit findings in redaction, signature validation, and untrusted-input handling (#1099)
* fix(create): cap decoded size of imported PNGs

A PNG's declared dimensions sized an infallible allocation, so a few-byte file claiming huge dimensions aborted the process when the allocation failed. Cap the decoded size at the TIFF ceiling and reserve every input-sized buffer fallibly, returning CreateError::Image instead. (Security audit run-2: v3-024.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(xfa): bound total picture bytes kept during layout

Each laid-out picture kept its own copy, so one picture in a repeating subform multiplied without bound and could exhaust memory on open. Add a per-layout aggregate cap (256 MiB) that stops with XfaError::TooLarge. (Security audit run-2: v3-103.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(xfdf): bound XML nesting and protect redaction marks on import

Deeply nested XFDF/XML overflowed roxmltree's recursive parser and aborted the process; a non-recursive depth scan now rejects it before parsing. Imported comments could also replace a locked or redaction mark by /NM or add a hidden one; place() no longer replaces locked/Redact annotations and clears the hidden/invisible flags on imported redaction marks. (Security audit run-2: v3-105, v3-111.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sign): classify post-signing changes by the signed revision's roles

A signed object's modification was classified from labels in the attacker-written new version, so relabelling a signed page /Type /Sig (or giving it /ModDate), or relabelling a drawn stream /ObjStm or /XRef, let a forged page validate as Valid even under DocMDP P=1. Decide an object's role (page content, signature, metadata, file plumbing) from what it was in the signed revision, not its new dictionary. (Security audit run-2: v3-082, v3-083.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(redact): remove redacted content from the saved file, fail closed otherwise

Apply Redactions and Remove Hidden Information left the originals behind: dropped or cleaned image/form XObjects kept their resource names (so the full rewrite saved the originals), an XFA form kept the redacted value in its datasets packet, and vertical-writing text under a mark was never hit by the horizontal removal geometry yet reported success. Prune replaced XObject names the page no longer draws, drop /XFA and /NeedsRendering when redacting an XFA form, and fail closed (RedactError::VerticalText) rather than silently leave vertical text. (Security audit run-2: v3-064, v3-067, v3-062.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ui-egui): trust the displayed signer, not the document's embedded root

The Signatures panel's one-click 'Add to trusted certificates' trusted the top of the chain the document supplied, installing the author's self-signed root as an anchor instead of the signer shown. Trust the displayed signer certificate. (Security audit run-2: v3-098.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Use main's decoded-size ceiling for PNGs too; keep the depth tests on main's checker

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 15:09:39 -04:00
snipeTR 9b3b98d2e2 fix(redact): the Email pattern finds internationalized addresses (#901)
The Email pattern of Search & Redact accepted only ASCII letters and
digits, so addresses such as "ayşe.yılmaz@örnek.com.tr", "müller@bücher.de"
or "иван@пример.рф" (RFC 6531 local parts, IDN domains) were not found
and stayed visible after a redaction by pattern.

Letters and digits of any script written with spaces now count in the
local part, the domain and the top-level domain. Scripts written without
spaces (Chinese, Japanese, Korean, Thai, …) still don't, so text that
runs straight into an address, as in "メールはtaro@example.jpまで", is not
swallowed into it. A domain still needs a dot and an alphabetic top-level
domain of two or more letters.
2026-10-10 17:04:41 -04:00
snipeTR d1d003c318 fix(redact): Turkish, German, French and Italian month names in the Dates pattern (#900)
The Dates pattern of Search & Redact knew only English month names, so
"10 Ekim 2026", "3 Mayıs 2025" or "5. Oktober 2026" stayed visible after a
redaction by pattern. It also lowercased with str::to_lowercase, under
which "MAYIS" never equals "mayıs" and "EKİM" grows a combining dot.

Month names are now listed for English, Turkish, German, French and
Italian, with English and Turkish abbreviations ("Şub.", "Ağu"), and are
compared one character at a time with I, i, İ and ı folded to i, so any
case matches. A day may end in a full stop before the month name
("5. Oktober 2026"). Words that only begin like a month ("Martı",
"Ekimde", "Oktoberfest") and month names without a day and year still
don't match.
2026-10-10 17:04:27 -04:00
9cd07e4793 M8: redact lists of words or phrases (protect.redact-word-lists) (#757)
* M8: redact lists of words or phrases

Find text and redact gains "Multiple words or phrases": a list with one word
or phrase per line, typed or imported from a .txt file (Import list…), whose
every match is marked in one undo step. Lists are trimmed, deduplicated and
bounded (1,000 entries of 256 characters; files up to 1 MB are read).
redact_mark takes words and reports matched_words, the marks each entry made.

protect.redact-word-lists is shipped; whole-word and case-sensitive matching
and exporting the list are still missing.

* M8: translate word-list labels into Arabic

The Arabic catalog arrived on main after this branch, so the new Find
text and redact strings were still English and failed
arabic_covers_ui_literals.

* Tests: drop sharp grid renders when the thumbnail will cover the cell

Zooming the organize grid back out left the sharp page raster in place
until a thumbnail texture existed, so the zoom test failed when that
texture had not arrived yet.

* M8: Czech rows for the word-list redaction labels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:32:09 -04:00
bfa65a9bfc M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text (#741)
* M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text

Redaction properties offers "Custom text" or "Redaction code" under "Use overlay
text". Picking the U.S. FOIA (5 U.S.C. 552(b)) or U.S. Privacy Act (552a) set and
ticking codes makes new marks carry them as overlay text, in the set's order.
redact_mark takes code_set and codes and rejects unknown sets or codes with the
allowed list.

Also merges a duplicated evidence/notes pair in form.field-list that made
parity/acrobat-features.toml fail to parse on main.

* M8: say why the default redaction code set can't fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: pin the clock in form_bulk (saves a second apart differed in /ModDate on Windows CI)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:27:26 -04:00
Brandon ThomasandClaude Opus 5.5 72d640f9f0 Fix #155: edit text across content stream pieces (#628)
A page's /Contents array is one content stream in pieces that writers may
split between any two tokens (ISO 32000-2 §7.8.2). Edit text parsed each
piece on its own, so an operator whose operands ended one piece and whose
keyword started the next arrived with no operands: a `[(After) -20 (wards)]`
/ `TJ` split made the line vanish from text_lines, text_paragraphs and
text_edit, and from the Edit text tool.

The joined-parse logic the redaction interpreter used (#153) moves into
pdfcraft-content as `Pieces`: join the pieces, parse them as one stream,
map positions back to pieces, and splice joined byte ranges back into
per-piece bytes (an edit replacing or removing an operator that spans
pieces rewrites every piece it touches; untouched pieces keep their
objects). text_lines now interprets the joined operators, and replace_line
and rewrite_block splice through `Pieces`. Redaction uses the same helper.

Lines and paragraphs are grouped by the piece their first operator starts
in, as before, so pages whose pieces are separate drawings don't merge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:57:02 -04:00
Brandon ThomasandClaude Opus 5.5 bd57377435 Fix #525 (item 4): phone pattern redacts international numbers whole (#543)
The Phone Numbers redaction pattern accepted only 10 digits (11 with a
country code), the North American form. For "+49 2151 123456" (12 digits)
the `+` candidate was rejected and the scan then matched "2151 123456",
leaving "+49" visible: a partial redaction in a document the user thinks
is clean.

The pattern now accepts, still grouped by at least one separator:
- E.164 numbers with a leading `+`: 8-15 digits in all;
- national numbers with a trunk 0 followed by 6-14 digits, whose first
  group has at least two digits (so decimals don't match) and which are
  not a day-first date (left to the date pattern);
- North American numbers as before.
A separator may now be followed by `(`, so "+49 (0) 2151 123456" is one
match instead of a partial "(0) 2151 123456".

Refs #525. The other items (page-number and date tokens, stamps) are not
addressed here.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:43:11 -04:00
7920f99f7a Redact: operators split across a page's content streams are redacted whole (#153)
* Redact: operators split across a page's content streams are redacted whole

A page's /Contents array is one content stream in pieces, and a writer
may split it between any two tokens (ISO 32000-2 §7.8.2). Some office
and layout exporters do: a TJ array ends one piece and its TJ operator
starts the next, or a marked-content dictionary closes in the next piece.

Redaction parsed each piece on its own, so such an operator was cut off
from its operands. Glyphs shown by a split TJ were never checked against
the marks: Apply drew the black box but left the text in the file, and
Verify reported nothing under the box. When the piece holding the
operands was rewritten for another mark, they were dropped, so the text
after it vanished and the next piece began with an operator without
operands, which other readers reject.

process() now parses the pieces joined, gives each operator back to the
piece its keyword is in, and rewrites every piece a split operator spans.
Pages whose pieces split only between operators are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Use the pdfcraft crate names after the rename

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 21:12:27 -04:00
Brandon ThomasandClaude Opus 5.5 d3db99818f Rename PrintCraft to PdfCraft (#174)
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:03:57 -04:00
Brandon ThomasandClaude Opus 5.5 1434db626a Never crash: deny the panic lints in crates that are already clean
annot, compare, content, cos, export, geom, model, ocr, preflight, print and redact have no
production `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!`/`unimplemented!`. The attribute
keeps it that way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:12:21 -04:00
Brandon ThomasandClaude Opus 5.5 ab47bf4b9a CI: as_chunks instead of constant-size chunks_exact (clippy 1.99 chunks_exact_to_as_chunks)
Stable clippy 1.99 denies chunks_exact/chunks_exact_mut with a constant
size (32 sites), which failed the test job on main and every open PR.
Behaviour unchanged; MSRV 1.90 has as_chunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:48:43 -04:00
storybored379andClaude Opus 5.5 0523ccdfdc M8: Redaction drops /PCAdded from rewritten content streams so redacted added text does not stay in the file (#1)
Text added with Edit ▸ Add content keeps its source string in the stream dictionary's /PCAdded.
Applying a redaction rewrote the stream's glyphs but copied the dictionary unchanged, so the
redacted text survived in the saved PDF. The redacted item now stays as plain drawn content and
is no longer editable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:14:56 -04:00
Brandon ThomasandClaude Opus 5.5 5a70838884 M7: PDF font tables move to the fonts crate with Unicode decoding/encoding (ToUnicode, WinAnsi/MacRoman/Standard, Differences glyph names, subset glyph checks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:42:57 -04:00
Brandon ThomasandClaude Opus 5.5 beb4e14c44 xtask: version test uses gates::root (fixes the xtask test build)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:42:57 -04:00
Brandon ThomasandClaude Opus 5.5 294cbd6716 M8: Applying redactions cleans the tags: elements of changed marked content lose Alt/ActualText/E, empty marked content leaves them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:48:43 -04:00
Brandon ThomasandClaude Opus 5.5 17e3eb255d M8: Redaction Properties: overlay text font (Helvetica, Times, Courier), size or auto-size, colour, alignment and repeat, written as /DA, /Q and /Repeat and drawn on apply; overlay fonts are added to the page only when used
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:33:01 -04:00
Brandon ThomasandClaude Opus 5.5 d2926538bd M8.4: Remove Hidden Information and Sanitize Document: scan with per-category counts (metadata, attachments, comments, form fields, hidden text in render modes 3/7 or off the page, hidden layers' content and groups, bookmarks, links/actions/JavaScript, private data); remove chosen categories (fields flattened so values stay visible) or all; full rewrite on save. Dialogs with checkboxes and counts, Sanitize confirmation; doc_hidden_info/doc_remove_hidden tools (59 tools)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:30:18 -04:00
Brandon ThomasandClaude Opus 5.5 f8175fb97e M8.5/M8.6/M8.7: Redact a PDF: new content crate (L2: content-stream operators with spans, inline images, serializer, matrices) and redact crate (L4): Redact annotations; apply removes glyphs (TJ displacement keeps the rest in place; simple, composite/CMap and Type 3 fonts), clears covered image pixels (or removes images it can't re-encode), removes or clips vectors and shadings, rewrites form XObjects copy-on-write, removes overlapping comments, links and fields, draws the boxes with overlay text, then verifies and fails closed; redacted documents save as a full rewrite. Search & Redact text and patterns (phone, e-mail, credit card, SSN, dates). UI: Redact tool (text or box), Redact pages, Find text and redact, Set properties, Clear all / Redact all with confirmation; redact_mark/redact_apply/redact_clear tools (57 tools). Fix: deleting a field when /AcroForm is a direct dictionary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:24:29 -04:00