Naming history: PXA has been renamed to PVAD (technical name). PVADE is the market name. PDF uses PVADP and Spreadsheet uses PVADS. Historical PXA/Canvas product references, implementation notes and compatibility identifiers are retained; task statuses are unchanged.
Legacy milestone record. The current implementation supersedes the V1 decisions below: new encrypted Generator, Designer, and WebApi output defaults to PDF 2.0 AES-256 revision 6;
Rc4_128is an explicit legacy option, AES-128 is an interoperability bridge, and X.509 recipient encryption is available throughPdfCertificateEncryptionOptions. SeePVAD.PDF-AES256-Security-Handler.mdfor the current security contract.
Current delivered coverage:
- AES-256 revision 6 is the default for new encrypted production PDFs.
- AES-128 and RC4-128 remain explicit interoperability and legacy options.
- Existing AES-256, AES-128, and RC4 documents can be opened through the importer with user/owner authentication and permission inspection.
- Wrong passwords, tampered permission blocks, Unicode passwords, all supported permission combinations, encrypted metadata, object streams, replacement encryption, and signed-document safety are covered by automated tests.
- X.509 recipient encryption is available as a separate public-key profile.
- The repeatable open-source interoperability matrix passes Unicode user/owner,
wrong-password, metadata, and extraction checks with qpdf and Poppler (
7/7). - Complete the wider external reader/writer interoperability matrix before a production support claim is expanded beyond the documented qpdf baseline.
Everything below this point is retained as the historical V1 implementation record. Its RC4-first scope and deferred AES decisions no longer describe the current product.
V1 shipped (RC4-128). PdfSaveOptions.Encryption encrypts strings and streams with the Standard
Security Handler (/V 2 /R 3). Verified end-to-end: PdfPig opens the output with the password and
recovers the text. AES-128 (/V 4 /R 4) is deferred to V2 — RC4 was chosen first because, as a
stream cipher, it preserves byte length and avoids rewriting every /Length in the already-serialized
object bodies. The handler is structured so AES slots in as an additional mode.
Add document encryption to PXA.Pdf so generated PDFs can be password-protected with owner/user
passwords and permission flags — the clearest functional gap versus DevExpress PDF
(PdfEncryptionOptions). Today PdfSaveOptions.cs only exposes
CompressContentStreams and CollectDiagnostics; there is no encryption anywhere in PXA/Pdf/.
Target: the Standard Security Handler. V1 ships RC4-128 (revision 3, /V 2 /R 3); AES-128
(revision 4, /AESV2) is deferred to V2. AES-256 (PDF 2.0, rev 6) is explicitly out of scope.
- V1: encrypt strings and streams in newly generated documents only (PXA.Pdf is generation-only).
- V1: owner password, user password, permission flags, RC4-128. (AES-128 deferred to V2.)
- Out of scope: AES-256 / PDF 2.0 rev 6, public-key (certificate) security, decrypting/re-saving existing PDFs, digital signatures.
- New
PdfEncryptionOptionsclass:-
string? UserPassword(open-document password; empty = openable without prompt). -
string? OwnerPassword(permissions password; defaults to user password if unset). -
PdfPermissions Permissions([Flags]: Print, Modify, Copy, AnnotateAndFillForms, FillFormsOnly, ExtractForAccessibility, Assemble, PrintHighResolution). -
PdfEncryptionAlgorithm Algorithm(Rc4_128default;Aes128present but throwsNotSupportedExceptionuntil V2).
-
- Extend PdfSaveOptions.cs with
PdfEncryptionOptions? Encryption { get; init; }. -
PdfDocument.Save(...)/ToBytes(...)already acceptPdfSaveOptions, so no new overloads needed — encryption flows through existing options.
-
Rc4.cs— hand-rolled RC4 (≈15 lines; not in .NET BCL). Used both for RC4 mode and inside the rev-3/O//Ukey algorithms even when the data mode is AES. - Reuse
System.Security.Cryptography.MD5from the BCL. (BCLAesreserved for V2.) -
StandardSecurityHandler.csimplementing the PDF 32000-1 algorithms:- Algorithm 2 — compute the file encryption key from padded user password +
/O+/P+ first/IDelement (+ rev-4 metadata byte). - Algorithm 3 — compute
/Ofrom owner & user passwords. - Algorithm 4/5 — compute
/U(rev 2 vs rev 3+). - Algorithm 1 — per-object key = MD5(fileKey + objNum(3 bytes) + gen(2 bytes) [+ "sAlT" for AES]),
truncated to
min(keyLen+5, 16); then RC4 or AES-CBC the data.
- Algorithm 2 — compute the file encryption key from padded user password +
Writer integration (PdfWriter.cs)
The writer builds PdfIndirectObjects then Serializes them. Encryption must touch every string and
stream except the /Encrypt dict and the /ID. Because object bodies are currently assembled as
opaque strings, this is the most invasive part.
- Add a
/IDarray (two 16-byte hex strings, derived from file metadata/time) to the trailer inSerialize— prerequisite: encryption keys depend on the first/IDelement. (Currently the trailer has/Size /Root /Infoonly.) - Pass a
StandardSecurityHandler?(null when no encryption) intoSerialize, which rewrites each object body just before writing — viaEncryptObjectBody(objNum, gen, bytes):- in non-stream objects, every literal
( ... )string is RC4'd with the per-object key (escaped in/out; RC4 preserves length so xref offsets recomputed from actual write positions stay valid); - in stream objects, the payload (located via
/Length) is RC4'd in place — length unchanged; - the
/Encryptdictionary object is skipped (not encrypted); the/IDlives in the trailer.
- in non-stream objects, every literal
- Emit the
/Encryptindirect object<< /Filter /Standard /V 2 /R 3 /Length 128 /P <signed-int> /O (...) /U (...) >>and reference it from the trailer (/Encrypt n 0 R). (rev-4/CF /AESV2deferred to V2.)
- Auto-rewrite the common encryption shape: pre-scan
new PdfEncryptionOptions()+UserPasswordString/OwnerPasswordStringassignments +new PdfSaveOptions { EncryptionOptions = ... }+ two-argSaveDocument(path, saveOptions), drop those source statements, and emitdocument.Save(path, new PdfSaveOptions { Encryption = new PdfEncryptionOptions { UserPassword = ..., OwnerPassword = ... } })(CANMIGDEVEXP010). Also removes consumedDXFontdeclarations (CANMIGDEVEXP025). - Fallback: when encryption is present but not auto-mappable (no two-arg
SaveDocument), emit theCANMIGDEVEXP024guidance warning instead. - Deferred: map DevExpress
Permissionsenum values to PXAPdfPermissions(passwords map now; permissions still need manual translation). - Update Code-Migration-DevExpressPdf.md: encryption moves from "manual" warning to a real conversion.
-
Rc4round-trip and known-answer vector. -
StandardSecurityHandler/O,/U, file-key against PDF-spec known answers. - Save with user password → file has
/Encrypt,/ID; strings/streams are not plaintext-readable. - Save with AES-128 vs RC4-128 → correct
/V//R//CFvalues. - Permission flags serialize to the correct signed
/Pbit pattern. - Interop: open an encrypted output with an existing reader (e.g. PdfPig / qpdf) using the password to confirm real-world decryptability — the key correctness gate.
- Regression: unencrypted save (no
Encryption) is byte-stable apart from the new/ID. - DevExpress migration: encryption sample converts to
PdfSaveOptions { Encryption = ... }.
dotnet testforPXA.Infrastructure.Pdf.Tests(or the PXA.Pdf test project) — crypto + writer.dotnet build PXA.sln.- Manual: generate a password-protected PDF, open it in a real viewer, confirm it prompts for the password and that permission flags are honoured.
- Primary algorithm tier — RC4-128 (rev 3) for V1; AES-128 deferred to V2 (avoids
/Lengthrewrite on opaque object bodies). AES-256 out of scope. -
/IDderivation —MD5(GUID + UTC ticks + document title); both/IDelements identical.
- AES-128 (
/V 4 /R 4,/AESV2): per-object AES-CBC with random IV,/CFcrypt filter, and/Lengthrewrite for the IV+padding growth. - Full DevExpress encryption auto-rewrite (see migration wire-back above).