Technical Article

Audit PDF Encryption and Permissions in Delphi: PDFlibPas

A document intake pipeline once quarantined an entire batch of legal filings as 'encrypted' even though every file opened in Adobe Acrobat without a password prompt. The probe had reduced security state to a single boolean. The files did carry an /Encrypt dictionary — but its crypt filters were set to Identity, meaning the strings and streams were stored in plaintext. Formally encrypted, practically open, and blocked for two days on the strength of one misread flag. That incident is a good definition of what a real encryption audit has to be: not 'is it encrypted,' but which algorithm, which revision, which passwords, which permissions, and which parts of the file the encryption actually touches. PDFlibPas, losLab's PDF engine for Delphi and C++Builder, exposes all of those answers through both a flat API and a typed class layer.

UK teams should align this pdflibpas encryption permissions audit workflow with local governance, audit, and data quality requirements before production release

For UK environments, align the pdflibpas encryption permissions audit implementation with local quality gates that include governance approval, versioned fixture baselines, and evidence retention for every publication candidate. Keep an explicit review log for accessibility, redaction policy, and data residency checks so your deployment audit is repeatable without changing output binaries

UK technical governance addendum

What the /Encrypt dictionary actually records

ISO 32000-1 §7.6 defines document security through a handful of dictionary entries, and PDFlibPas mirrors them one-for-one in the TPDFEncryption record: the filter version V and revision R that select the algorithm family, Length for the key size, the permission bits in P, the owner and user password validation strings O and U (plus OE/UE for AES-256), an EncryptMetadata flag, and the names of the crypt filters applied to strings, streams, and embedded files.

The record is the auditor's friend precisely because it does not interpret. The intake incident above becomes visible in fields like StringFilterIdentity and StreamFilterIdentity: when those are true, the corresponding data is not transformed at all, whatever the document's encrypted status claims. Likewise EncryptMetadata = False tells you the XMP metadata is readable by any indexer even though page content is not — relevant when routing rules depend on title or author fields.

A ten-line security probe with the flat API

For most pipelines, four flat calls answer the everyday questions. LoadFromFile returns 1 on success; after that the document-level inspectors work against the decrypted state:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', UserPassword) <> 1 then
      raise Exception.Create('Open failed: wrong password or damaged file');
    Writeln('status    : ', PDF.EncryptionStatus);     // decrypted / encrypted / unknown
    Writeln('algorithm : ', PDF.EncryptionAlgorithm);  // RC4 vs AES family
    Writeln('strength  : ', PDF.EncryptionStrength);   // key length class
    Writeln('owner pw? : ', PDF.CheckPassword(CandidatePassword));
  finally
    PDF.Free;
  end;
end;

CheckPassword matters more than it looks. PDF distinguishes a user password (required to open) from an owner password (grants full rights and overrides permissions), and a file opened with the user password behaves very differently from one opened with the owner password — same bytes, different rights. The class layer makes the distinction explicit: TPDFDocument.HasUserPassword and HasOwnerPassword report what is set, whilst IsUserPassword and IsOwnerPassword report which one actually opened the current session. An audit log should record that distinction, never the password values themselves.

The Strength ladder: from 40-bit RC4 to AES-256 revision 6

The flat Encrypt and EncryptFile functions take an integer Strength with five meaningful values: 0 for 40-bit RC4, 1 for 128-bit RC4, 2 for 128-bit AES (readable from Acrobat 7), 3 for 256-bit AES as introduced with Acrobat 9, and 4 for 256-bit AES as required by Acrobat X and later.

Values 3 and 4 deserve a closer look, because 'AES-256' is two different schemes. Strength 3 maps to security handler revision 5, an interim design that shipped in Acrobat 9 and was never adopted by ISO. Strength 4 maps to revision 6, the scheme with the hardened key-derivation function that ISO 32000-2 standardizes. For new documents there is no reason to choose 3; for audits, the difference matters because a compliance policy that says 'AES-256 per ISO 32000-2' is satisfied only by R6. On the reading side, the class layer separates the two as esAES256Bit versus esAES256BitAcroX, and the EncryptionAcroX property answers the R5-or-R6 question directly.

Permission bits and their key-length fine print

EncodePermissions packs eight flags into the integer that Encrypt and EncryptFile expect: print, copy, change, and add-notes form the basic set, whilst fill-fields, copy-for-accessibility, assemble, and full-quality print form the extended set. The fine print, spelled out in the library's own encryption demo, is that the extended four are only honored at 128-bit strength and above — and so is degrading print quality by setting the full-quality-print flag to 0. Encode a 'low-resolution print only' policy into a 40-bit document and viewers will simply print at full quality.

The deeper caveat is who enforces these bits. Permissions in PDF are instructions to conforming readers, not cryptographic restrictions: the decryption key is the same whether or not copying is allowed. A locked-down permission set keeps honest viewers honest. If your obligation is to prevent extraction instead of discourage it, that requires a user password and process-level controls, and your audit report should be explicit about which of the two regimes a file is under.

Setting policy and proving it stuck

Applying encryption to existing files does not require loading them into the object tree. EncryptFile processes input to output in one call, and the audit loop reopens the result to verify the outcome — a pattern lifted almost verbatim from the shipped demo:

var
  PDF: TPDFlib;
  R: Integer;
begin
  PDF := TPDFlib.Create;
  try
    R := PDF.EncryptFile('in.pdf', 'out.pdf', 'owner-secret', 'user-secret', 4,
      PDF.EncodePermissions(1, 0, 0, 0,    // print allowed; copy/change/notes denied
                            0, 0, 0, 1));  // extended set: full-quality print only
    if (R = 1) and (PDF.LoadFromFile('out.pdf', 'user-secret') = 1) then
    begin
      Writeln('algorithm = ', PDF.EncryptionAlgorithm);
      Writeln('strength  = ', PDF.EncryptionStrength);
      Writeln('owner pw accepted: ', PDF.CheckPassword('owner-secret'));
    end;
  finally
    PDF.Free;
  end;
end;

Teams working at the document layer get the same operation with typed sets instead of bit packing, which reads better in code review:

if not Doc.Encrypt('owner-secret', 'user-secret', esAES256BitAcroX,
  [ppCanPrint], [ppCanPrintFull]) then
  raise Exception.Create('Encryption failed');

Either way, the read-back step is not optional ceremony. It catches the classic deployment mistakes — an old library version that silently downgrades the requested strength, an output path that was never written, a permissions integer assembled in the wrong argument order — at the moment they happen instead of at the customer's desk. GetEncryptionFingerprint gives you a compact value to store with the job record for later comparison.

Audit false positives worth coding for

Three patterns repeatedly produce wrong conclusions in security scanners. Firstly, the Identity crypt filter case from the opening: an /Encrypt dictionary present, real content untouched — check the per-filter Identity flags before declaring data protected. Secondly, the metadata split: EncryptMetadata can disagree with the remainder of the file in both directions, so 'the file is encrypted' says nothing about whether the XMP packet is. Thirdly, embedded files: PDF allows a dedicated crypt filter for file attachments, so attachments may be the only encrypted part of an otherwise open document, or the only plaintext part of an encrypted one. An audit record that captures the three filter assignments separately — strings, streams, embedded files — is immune to all three traps; one that stores a boolean is wrong on schedule.

Questions auditors actually ask

Can I remove encryption from a file when I have the password? Yes — DecryptFile(InputFileName, OutputFileName, Password) does it without a full load, and the loaded-document Decrypt does the same in memory. Whether you may is a policy question your intake rules should answer explicitly.

Which strength should new documents use? Strength 4, AES-256 revision 6, unless you must support viewers older than Acrobat X. Strength 2 (AES-128) remains the pragmatic floor for very old viewer fleets; the RC4 options exist for compatibility auditing, not for new output.

Do permission flags stop a determined user from copying text? No. They are honored by conforming viewers, and ISO 32000 frames them as access permissions for readers to enforce, not as cryptography. Pair them with a user password when confidentiality is the actual requirement.

Further reading

Encryption state feeds directly into signing decisions — a workbench that validates and signs documents needs the same read-back discipline, as covered in the compliance and signing workbench article. For batch pipelines that apply EncryptFile across thousands of large documents, the direct-access guide to large PDFs shows how to keep memory flat whilst doing it.

The complete encryption API reference lives on the PDFlibPas product page.