Technical Article

PDF Permission Flags, the /P Integer and the /Perms Copy

You encrypt a report with a mask meant to allow everything, $FFFFFFF0, and Acrobat's Security tab lists printing and changing the document as not allowed. /P is an unsigned 32-bit flag word numbered from bit 1, so that mask clears print (bit 3) and modify (bit 4); the all-allowed value is $FFFFFFFC, the starting point of TPdfEncryptOptions in PDFium Component

How the /P flag word is laid out

Only eight positions carry meaning, the rest are reserved, and counting starts at 1. ISO 32000-1 §7.6.3.2 says P "shall be interpreted as an unsigned 32-bit quantity", numbered from 1 at the low-order end to 32 at the high-order end, and tells conforming readers to ignore every bit other than 3, 4, 5, 6, 9, 10, 11 and 12. ISO 32000-2 §7.6.4.2 corrects Table 22 and spells out the reserved positions: bits 1–2 must be 0, bits 7–8 must be 1, and for revision 3 and later bits 13–32 must be 1. PDF 2.0 also retires bit 10, which readers now ignore and writers always set to 1 for older readers; the accessibility carve-out moves to bit 5, so a reader should act as if copying were allowed when the content goes to assistive technology. One formula covers every row below. The mask for bit n is 1 shl (n - 1) — and TPdfPermission maps onto the word the same way

BitMaskMeaning (revision 3 and later)TPdfPermission
1–2$00000003Reserved, must be 0none
3$00000004Print, at a quality that depends on bit 12permPrint
4$00000008Modify contents by operations other than those of bits 6, 9 and 11permModify
5$00000010Copy or extract text and graphicspermCopy
6$00000020Add or modify annotations, fill fields; with bit 4, create fieldspermAnnotate
7–8$000000C0Reserved, must be 1none
9$00000100Fill existing form fields, including signature fields, even if bit 6 is clearpermFillForms
10$00000200Accessibility extraction; unused in PDF 2.0, always written as 1permAccessibility
11$00000400Assemble: insert, rotate or delete pages, create outline items and thumbnailspermAssemble
12$00000800Print to a faithful, high-quality representationpermPrintHighQuality
13–32$FFFFF000Reserved, must be 1 (ISO/TS 32004 assigns bit 13, see below)none

The $FFFFFFF0 mask comes from reading "the low bits are reserved" and clearing a whole nibble to be safe. The reserved region was only bits 1–2, mask $3; clearing $F also wipes out $4 and $8, which are print and modify, and any viewer that honours the flags will refuse both. The mirror-image mistake is assembling the mask upward from zero: OR together $4, $10 and $800 and you get a small positive /P with bits 7–8 and 13–32 clear. Most readers open that file without comment, because the specification tells them to ignore reserved bits, but the value is nonconforming for revision 3 and later and a strict checker is entitled to complain. A third variant shows up in code that counts bits from zero, where "bit 3" becomes $8 and the program grants modify when it meant print. None of these produce an error at save time. The permissions simply come out different from the ones the author intended

PDF /P permission flag word layout for PDFium Component in Delphi: reserved bits 1-2, 7-8 and 13-32, the TPdfPermission bits such as permPrint and permModify, and the masks $FFFFFFFC and $FFFFFFF0
Clearing the whole low nibble with $FFFFFFF0 also removes print and modify, while building the mask up from zero leaves the reserved ones of revision 3 and later clear

Is /P -4 the same value as /P 4294967292?

Yes. Same 32 bits, two spellings. PDF integer objects are signed, and the note just above Table 22 points out that because the reserved high-order bits are 1, the integer is written as a negative number; -4 is the conventional all-allowed value for revision 3 and later. Producers that keep the value in an unsigned variable write 4294967292 instead, and both spellings turn up in real files. The specification settles which reading is right in its own key derivation: step (d) of Algorithm 2 in ISO 32000-1 §7.6.3.3 converts P to a 32-bit unsigned binary number and passes the four bytes to MD5 low-order byte first, so for revisions 2 to 4 the values -3904 and 4294963392 produce the same file key. A parser that stores /P in a signed 32-bit Integer rejects the unsigned spelling as an overflow; switch the variable to Cardinal and the negative spelling fails instead, while code that compares the token text against a stored value ends up treating one document as two. Read the token into an Int64, accept anything from Low(Integer) to High(Cardinal), and mask to 32 bits before any comparison. PDFium Component's own decryption code reads /P that way for revisions 2 through 7, and its writer emits the signed form that the specification's note shows. The reserved-bit report in the snippet stops at bit 14, since a clear bit 13 is how ISO/TS 32004 marks a file that carries a PDF MAC

uses
  System.SysUtils;

// Normalise the /P token text from your own dictionary parser
function TryNormalizePermissions(const Token: string;
  out Mask: Cardinal): Boolean;
var
  Value: Int64;
begin
  Result := TryStrToInt64(Token, Value) and
    (Value >= Low(Integer)) and (Value <= High(Cardinal));
  if Result then
    Mask := Cardinal(Value and $FFFFFFFF);
end;

function DescribeReservedBits(Mask: Cardinal): string;
begin
  Result := '';
  if (Mask and $00000003) <> 0 then
    Result := Result + 'bits 1-2 set; ';
  if (Mask and $000000C0) <> $000000C0 then
    Result := Result + 'bits 7-8 clear; ';
  // Bit 13 ($1000) is left out: ISO/TS 32004 clears it to require a PDF MAC
  if (Mask and $FFFFE000) <> $FFFFE000 then
    Result := Result + 'bits 14-32 not all set; ';
end;

// Expected: both calls yield $FFFFFFFC and an empty reserved-bit report
//   TryNormalizePermissions('-4', M1);
//   TryNormalizePermissions('4294967292', M2);
Normalising the PDF /P value in Delphi as PDFium Component does: /P -4 and /P 4294967292 read into Int64, range-checked from Low(Integer) to High(Cardinal) and masked to the Cardinal $FFFFFFFC
Signed Integer and Cardinal parsers each reject one legal spelling of /P, and reading Int64 then masking to 32 bits maps both spellings to one value

Building a mask from a TPdfPermissions set

Start with every reserved bit at its required value and OR in only what you grant, so a forgotten permission can only ever turn into a denial. TPdfEncryptOptions.Default in the FPdfEncrypt unit already holds $FFFFFFFC, which allows everything, and clearing one or two bits from it works fine for a quick restriction. A policy that lists what recipients may do reads better as a set, and the constant array below lines up with the bit positions TPdf uses to decode the word. Force bit 10 on regardless of what the caller passes. ISO 32000-2 tells writers to always set it, PDF 2.0 readers ignore it anyway, and leaving it clear only risks blocking screen readers in older software

uses
  System.SysUtils, PDFium, FPdfEncrypt;

const
  PermissionBit: array[TPdfPermission] of Cardinal = (
    $0004,   // permPrint             bit 3
    $0008,   // permModify            bit 4
    $0010,   // permCopy              bit 5
    $0020,   // permAnnotate          bit 6
    $0100,   // permFillForms         bit 9
    $0200,   // permAccessibility     bit 10
    $0400,   // permAssemble          bit 11
    $0800);  // permPrintHighQuality  bit 12

  // Bits 7-8 and 13-32 set; bits 1-2 and every permission bit clear
  ReservedOnes = Cardinal($FFFFF0C0);

function PermissionsToMask(Allowed: TPdfPermissions): Cardinal;
var
  P: TPdfPermission;
begin
  Result := ReservedOnes or PermissionBit[permAccessibility];
  for P in Allowed do
    Result := Result or PermissionBit[P];
end;

procedure SaveFillAndPrintCopy(const Source, Target: string;
  const OwnerPassword: AnsiString);
var
  Pdf: TPdf;
  Opts: TPdfEncryptOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := Source;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.CreateFmt('Could not open %s', [Source]);

    Opts := TPdfEncryptOptions.Default;   // Revision = erR5 here
    Opts.UserPassword := '';              // anyone may open the file
    Opts.OwnerPassword := OwnerPassword;  // guards the permission set
    Opts.Revision := erR6;
    Opts.Permissions := PermissionsToMask(
      [permPrint, permPrintHighQuality, permFillForms]);

    // Expected: /P -1084 = 4294966212 ($FFFFFBC4)
    Writeln('/P ', Integer(Opts.Permissions), ' = ', Opts.Permissions);

    if not Pdf.SaveAsEncrypted(Target, Opts) then
      raise Exception.Create('Encrypted save failed');
  finally
    Pdf.Free;
  end;
end;

Expected: the Encrypt dictionary of the output carries /P -1084, and a viewer that honours the flags offers printing at full quality and form filling while greying out content edits, copying, commenting and page assembly. Bit 13 is one position the writer manages for you. With EnableIntegrityProtection left at False, SaveAsEncrypted sets bit 13 whatever your mask says; with it set to True, the writer clears bit 13 in both /P and /Perms. The other reserved bits stay your responsibility — hence the $FFFFF0C0 starting point. The example chooses erR6 because R6 is the AES-256 revision ISO 32000-2 standardises; the default record carries erR5

/Perms, the encrypted copy of /P in AES-256 files

In revisions 2 to 4 the permission word is welded to the key: change one bit of /P and the MD5 input changes, the derived key is wrong, and nothing decrypts. AES-256 files break that link. Algorithm 2.A in ISO 32000-2 §7.6.4.3.3 unwraps the file encryption key from /UE or /OE, and P plays no part in that derivation, so on its own a plaintext /P could be edited freely. /Perms closes the gap. Algorithm 10 (§7.6.4.4.9) fills a 16-byte block and encrypts it with the file key using AES-256 in ECB mode:

  • bytes 0–3: the permission word, low-order byte first, extended to 64 bits by bytes 4–7, which are all $FF
  • byte 8: ASCII T or F, matching /EncryptMetadata
  • bytes 9–11: the ASCII marker adb
  • bytes 12–15: random data that readers ignore

Step (f) of Algorithm 2.A decrypts the block, verifies adb and requires bytes 0–3 to match P — shall, not should. Algorithm 13 (§7.6.4.4.12) repeats the marker and permission checks with the softer should and adds one more: byte 8 against EncryptMetadata. PDFium Component reads it the strict way wherever its own code opens the encryption layer: AESV4 decryption, PDF MAC validation and incremental saves into encrypted files. After the user or owner password unlocks the file key, that code compares all four permission bytes against the normalised /P, checks the $FF filler, the T/F byte and the marker, and fails the load with "PDF encryption permissions validation failed" on any difference, since a mismatch means someone edited the dictionary after encryption and neither copy can be taken at face value

AES-256 /Perms block in PDFium Component: the /P permission word low-order byte first, $FF filler, the /EncryptMetadata T or F byte and the adb marker, encrypted by Algorithm 10 and verified under Algorithms 2.A and 13
Because the AES-256 file key no longer depends on /P, the decrypted /Perms bytes are what proves the plaintext permission word was not edited after encryption

The EncryptMetadata comparison deserves care in any reader you write. A shortcut like Pos('/EncryptMetadata false', Dict) > 0 misses /EncryptMetadata false with two spaces, a line break between key and value, or a comment in between, all legal PDF syntax. The reader then assumes the default true, finds F in a perfectly valid /Perms, and rejects the file. A valid file, refused. Parse the key and its value as tokens, and treat a non-boolean value as malformed rather than defaulting it

ISO/TS 32004 gives bit 13 a job: when it is clear, the document carries a PDF MAC that readers must check. The MAC token sits in the trailer's /AuthCode dictionary, which is not encrypted, so a check that only looks for /AuthCode concludes that a file with it deleted was never integrity-protected. Deleting /KDFSalt as well does not hide the requirement, because the cleared bit lives in /P and /P is authenticated by /Perms. PDFium Component's AESV4 decryption therefore treats a clear bit 13, or a leftover /KDFSalt, without an /AuthCode as a stripped MAC and fails, and when integrity protection is off the writer sets bit 13 so its output never announces a MAC it lacks. The key hierarchy and the byte range behind that token belong to AES-GCM encryption and the ISO/TS 32004 MAC in PDF 2.0, and the iterated password hash that unlocks the file key before any of this runs is covered in the R6 password hash and its round count

Permissions or UserPermissions: which one should your UI read?

Read Permissions to decide what the current session may do, and UserPermissions to report what the file grants an ordinary recipient. TPdf.Permissions wraps PDFium's FPDF_GetDocPermissions and describes the access this open document was granted, which depends on the password that unlocked it. TPdf.UserPermissions wraps FPDF_GetDocUserPermissions and keeps reporting the user-access grant from /P no matter which password was used, so the two can differ when the owner password opened the file. Both are LongWord; GrantedPermissions and UserGrantedPermissions decode them into TPdfPermissions sets, and HasPermission and UserHasPermission test a single member. SecurityHandlerRevision tells you whether the file has a security handler at all. The routine below prints both integer views of the raw word next to the decoded set, a quick way to see why a file "will not print"

uses
  System.SysUtils, System.TypInfo, PDFium;

procedure DumpPermissions(const FileName, Password: string);
var
  Pdf: TPdf;
  Mask: LongWord;
  Granted: TPdfPermissions;
  P: TPdfPermission;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Password := Password;   // set before Active
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.CreateFmt('Could not open %s', [FileName]);

    if Pdf.SecurityHandlerRevision <= 0 then
    begin
      Writeln('Not encrypted: no /P to decode');
      Exit;
    end;

    Mask := Pdf.UserPermissions;
    Granted := Pdf.UserGrantedPermissions;
    Writeln(Format('R%d  /P as Integer %d, as Cardinal %u ($%.8x)',
      [Pdf.SecurityHandlerRevision, Integer(Mask), Mask, Mask]));

    for P := Low(TPdfPermission) to High(TPdfPermission) do
      Writeln(Format('  %-22s %s',
        [GetEnumName(TypeInfo(TPdfPermission), Ord(P)),
         BoolToStr(P in Granted, True)]));

    if not Pdf.HasPermission(permCopy) then
      Writeln('  this session may not copy text');
  finally
    Pdf.Free;
  end;
end;

// Expected for the file written earlier:
//   R6  /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
//   permPrint True, permModify False, permCopy False, ...

Enforcement belongs to the host application

PDFium renders, extracts text and rasterises pages whatever the flags say. The engine reports permissions and leaves policy to the caller. Anyone holding the user password holds the decrypted content — with an empty user password, that is everyone — so confidentiality can only come from a user password the reader lacks. Put the check inside each command handler (copy, export, page deletion, print) rather than hiding toolbar buttons, since shortcuts, context menus and automation all bypass a toolbar, the same layering used in a secure PDF preview pane for Delphi apps. A print command, for instance, tests permPrint before it spools anything

In an AES-256 file, -1084 in plain text proves nothing until the first four decrypted bytes of /Perms read C4 FB FF FF. That byte-for-byte comparison runs inside the AESV4, PDF MAC and encrypted incremental-save code of PDFium Component