Technischer Artikel

PDF-Permission-Flags, der /P-Integer und die /Perms-Kopie

Sie verschlüsseln einen Bericht mit einer Maske, die alles erlauben soll, $FFFFFFF0, und Acrobats Security-Tab listet Drucken und das Dokument ändern als nicht erlaubt. /P ist ein vorzeichenloses 32-Bit-Flag-Wort, nummeriert ab Bit 1, also löscht diese Maske print (Bit 3) und modify (Bit 4); der alles-erlaubende Wert ist $FFFFFFFC, der Startpunkt von TPdfEncryptOptions in PDFium Component

Wie das /P-Flag-Wort aufgebaut ist

Nur acht Positionen tragen Bedeutung, der Rest ist reserviert, und das Zählen beginnt bei 1. ISO 32000-1 §7.6.3.2 sagt, P "shall be interpreted as an unsigned 32-bit quantity", nummeriert von 1 am low-order-Ende bis 32 am high-order-Ende, und heißt konforme Reader, jedes Bit außer 3, 4, 5, 6, 9, 10, 11 und 12 zu ignorieren. ISO 32000-2 §7.6.4.2 korrigiert Tabelle 22 und buchstabiert die reservierten Positionen aus: Bits 1–2 müssen 0 sein, Bits 7–8 müssen 1 sein, und für Revision 3 und später müssen Bits 13–32 1 sein. PDF 2.0 schaltet außerdem Bit 10 ab, das Reader jetzt ignorieren und Writer für ältere Reader immer auf 1 setzen; das Accessibility-Privileg wandert zu Bit 5, sodass ein Reader handeln soll, als wäre Kopieren erlaubt, wenn der Inhalt an Assistive Technology geht. Eine Formel deckt jede Zeile unten ab. Die Maske für Bit n ist 1 shl (n - 1) — und TPdfPermission landet auf dem Wort genauso

BitMaskeBedeutung (Revision 3 und später)TPdfPermission
1–2$00000003Reserviert, muss 0 seinkeine
3$00000004Print, in einer Qualität, die von Bit 12 abhängtpermPrint
4$00000008Inhalte durch Operationen ändern, die nicht die von Bits 6, 9 und 11 sindpermModify
5$00000010Text und Grafiken kopieren oder extrahierenpermCopy
6$00000020Annotationen hinzufügen oder ändern, Felder füllen; mit Bit 4, Felder anlegenpermAnnotate
7–8$000000C0Reserviert, muss 1 seinkeine
9$00000100Bestehende Formularfelder füllen, eingeschlossen Signaturfelder, selbst wenn Bit 6 clear istpermFillForms
10$00000200Accessibility-Extraktion; ungenutzt in PDF 2.0, immer als 1 geschriebenpermAccessibility
11$00000400Assemble: Seiten einfügen, drehen oder löschen, Outline-Einträge und Thumbnails erzeugenpermAssemble
12$00000800In einer originalgetreuen, hochwertigen Darstellung druckenpermPrintHighQuality
13–32$FFFFF000Reserviert, muss 1 sein (ISO/TS 32004 belegt Bit 13, siehe unten)keine

Die $FFFFFFF0-Maske kommt daher, "the low bits are reserved" zu lesen und vorsichtshalber ein ganzes Nibble zu löschen. Die reservierte Region war nur Bits 1–2, Maske $3; $F zu löschen beseitigt auch $4 und $8, was print und modify sind, und jeder Viewer, der die Flags beachtet, verweigert beides. Der spiegelbildliche Fehler ist, die Maske von null aufzubauen: $4, $10 und $800 per OR zusammen und heraus kommt ein kleines positives /P mit Bits 7–8 und 13–32 auf null. Die meisten Reader öffnen diese Datei ohne Murren, weil die Spezifikation ihnen sagt, reservierte Bits zu ignorieren, aber der Wert ist für Revision 3 und später nonkonform, und ein strenger Prüfer darf sich beschweren. Eine dritte Variante taucht in Code auf, der Bits ab null zählt, wo "bit 3" zu $8 wird und das Programm modify erlaubt, wo es print meinte. Keine davon erzeugt einen Fehler beim Speichern. Die Permissions kommen einfach anders heraus als vom Autor beabsichtigt

PDF /P-Permission-Flag-Wort-Layout für PDFium Component in Delphi: reservierte Bits 1-2, 7-8 und 13-32, die TPdfPermission-Bits wie permPrint und permModify, und die Masken $FFFFFFFC und $FFFFFFF0
Das ganze untere Nibble mit $FFFFFFF0 zu löschen beseitigt auch print und modify, während der Maskenaufbau von null die reservierten Einsen von Revision 3 und später auf null lässt

Ist /P -4 derselbe Wert wie /P 4294967292?

Ja. Dieselben 32 Bits, zwei Schreibweisen. PDF-Integer-Objekte sind vorzeichenbehaftet, und die Anmerkung direkt über Tabelle 22 weist darauf hin, dass der Integer als negative Zahl geschrieben wird, weil die reservierten high-order-Bits 1 sind; -4 ist der konventionelle alles-erlaubende Wert für Revision 3 und später. Producer, die den Wert in einer vorzeichenlosen Variablen halten, schreiben stattdessen 4294967292, und beide Schreibweisen tauchen in echten Dateien auf. Die Spezifikation entscheidet in ihrer eigenen Key-Ableitung, welche Lesart richtig ist: Schritt (d) von Algorithmus 2 in ISO 32000-1 §7.6.3.3 konvertiert P in eine 32-Bit-Unsigned-Binärzahl und reicht die vier Bytes an MD5 durch, low-order-Byte zuerst, also erzeugen für die Revisionen 2 bis 4 die Werte -3904 und 4294963392 denselben File-Key. Ein Parser, der /P in einem vorzeichenbehafteten 32-Bit-Integer speichert, verwirft die Unsigned-Schreibweise als Overflow; stellt man die Variable auf Cardinal, scheitert stattdessen die negative Schreibweise, und Code, der den Token-Text gegen einen gespeicherten Wert vergleicht, behandelt am Ende ein Dokument als zwei. Lies den Token in ein Int64, akzeptiere alles von Low(Integer) bis High(Cardinal), und maskiere auf 32 Bits vor jedem Vergleich. PDFium Components eigener Entschlüsselungscode liest /P für die Revisionen 2 bis 7 so, und sein Writer gibt die vorzeichenbehaftete Form aus, die die Anmerkung der Spezifikation zeigt. Der Reserved-Bit-Report im Snippet stoppt bei Bit 14, denn ein cleares Bit 13 ist es, womit ISO/TS 32004 eine Datei markiert, die einen PDF MAC trägt

uses
  System.SysUtils;

// Den /P-Token-Text aus Ihrem eigenen Dictionary-Parser normalisieren
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) bleibt außen vor: ISO/TS 32004 löscht es, um einen PDF MAC zu verlangen
  if (Mask and $FFFFE000) <> $FFFFE000 then
    Result := Result + 'bits 14-32 not all set; ';
end;

// Erwartet: beide Aufrufe liefern $FFFFFFFC und einen leeren Reserved-Bit-Report
//   TryNormalizePermissions('-4', M1);
//   TryNormalizePermissions('4294967292', M2);
Den PDF /P-Wert in Delphi normalisieren, wie PDFium Component es tut: /P -4 und /P 4294967292 in Int64 gelesen, von Low(Integer) bis High(Cardinal) reichengeprüft und auf den Cardinal $FFFFFFFC maskiert
Vorzeichenbehaftete Integer- und Cardinal-Parser verwerfen jeder je eine legale Schreibweise von /P, und Int64 zu lesen und dann auf 32 Bits zu maskieren mappt beide Schreibweisen auf einen Wert

Eine Maske aus einem TPdfPermissions-Set bauen

Beginnen Sie mit jedem reservierten Bit auf seinem geforderten Wert und per OR nur das hinein, was Sie gewähren, dann kann eine vergessene Permission nur zu einer Verweigerung werden. TPdfEncryptOptions.Default in der Unit FPdfEncrypt hält bereits $FFFFFFFC, was alles erlaubt, und ein oder zwei Bits davon zu löschen funktioniert für eine schnelle Einschränkung gut. Eine Policy, die auflistet, was Empfänger dürfen, liest sich besser als Set, und das Konstanten-Array unten passt zu den Bitpositionen, mit denen TPdf das Wort dekodiert. Erzwingen Sie Bit 10, egal was der Caller übergibt. ISO 32000-2 heißt Writer, es immer zu setzen, PDF-2.0-Reader ignorieren es ohnehin, und es offen zu lassen riskiert nur, Screenreader in älterer Software zu blockieren

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 und 13-32 gesetzt; Bits 1-2 und jedes Permission-Bit gelöscht
  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;   // hier Revision = erR5
    Opts.UserPassword := '';              // jeder darf die Datei öffnen
    Opts.OwnerPassword := OwnerPassword;  // bewacht das Permission-Set
    Opts.Revision := erR6;
    Opts.Permissions := PermissionsToMask(
      [permPrint, permPrintHighQuality, permFillForms]);

    // Erwartet: /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;

Erwartet: Das Encrypt-Dictionary des Outputs trägt /P -1084, und ein Viewer, der die Flags beachtet, bietet Drucken in voller Qualität und Formularausfüllen an, während Inhaltsänderungen, Kopieren, Kommentieren und Seiten-Assembly ausgegraut sind. Bit 13 ist eine Position, die der Writer für Sie verwaltet. Mit EnableIntegrityProtection auf False gelassen setzt SaveAsEncrypted Bit 13, was auch immer Ihre Maske sagt; auf True gesetzt löscht der Writer Bit 13 in /P und /Perms zugleich. Die anderen reservierten Bits bleiben Ihre Verantwortung — daher der $FFFFF0C0-Startpunkt. Das Beispiel wählt erR6, weil R6 die AES-256-Revision ist, die ISO 32000-2 standardisiert; der Default-Record trägt erR5

/Perms, die verschlüsselte Kopie von /P in AES-256-Dateien

In den Revisionen 2 bis 4 ist das Permission-Wort an den Key verschweißt: ändert man ein Bit von /P, ändert sich die MD5-Eingabe, der abgeleitete Key ist falsch, und nichts entschlüsselt. AES-256-Dateien brechen diese Verbindung. Algorithmus 2.A in ISO 32000-2 §7.6.4.3.3 wickelt den File-Encryption-Key aus /UE oder /OE aus, und P spielt in dieser Ableitung keine Rolle, also ließe sich ein Klartext-/P für sich genommen frei editieren. /Perms schließt die Lücke. Algorithmus 10 (§7.6.4.4.9) füllt einen 16-Byte-Block und verschlüsselt ihn mit dem File-Key per AES-256 im ECB-Mode:

  • Bytes 0–3: das Permission-Wort, low-order-Byte zuerst, auf 64 Bits erweitert durch die Bytes 4–7, die alle $FF sind
  • Byte 8: ASCII T oder F, passend zu /EncryptMetadata
  • Bytes 9–11: die ASCII-Markierung adb
  • Bytes 12–15: Zufallsdaten, die Reader ignorieren

Schritt (f) von Algorithmus 2.A entschlüsselt den Block, verifiziert adb und verlangt, dass Bytes 0–3 zu P passen — shall, nicht should. Algorithmus 13 (§7.6.4.4.12) wiederholt die Marker- und Permission-Prüfungen mit dem weicheren should und fügt eine weitere hinzu: Byte 8 gegen EncryptMetadata. PDFium Component liest es auf die strenge Weise, wo auch immer sein eigener Code die Encryption-Schicht öffnet: AESV4-Entschlüsselung, PDF-MAC-Validierung und inkrementelle Saves in verschlüsselte Dateien. Nachdem das User- oder Owner-Passwort den File-Key freigeschaltet hat, vergleicht dieser Code alle vier Permission-Bytes gegen das normalisierte /P, prüft den $FF-Füller, das T/F-Byte und die Markierung, und lässt das Laden mit "PDF encryption permissions validation failed" scheitern bei jeder Abweichung, denn eine Diskrepanz heißt, jemand hat das Dictionary nach der Verschlüsselung editiert, und keine der beiden Kopien kann man beim Wort nehmen

AES-256 /Perms-Block in PDFium Component: das /P-Permission-Wort low-order-Byte zuerst, $FF-Füller, das /EncryptMetadata-T-oder-F-Byte und die adb-Markierung, verschlüsselt von Algorithmus 10 und verifiziert unter den Algorithmen 2.A und 13
Weil der AES-256-File-Key nicht mehr von /P abhängt, sind die entschlüsselten /Perms-Bytes der Beweis, dass das Klartext-Permission-Wort nach der Verschlüsselung nicht editiert wurde

Der EncryptMetadata-Vergleich verdient Sorgfalt in jedem Reader, den Sie schreiben. Ein Shortcut wie Pos('/EncryptMetadata false', Dict) > 0 verpasst /EncryptMetadata false mit zwei Leerzeichen, einen Zeilenumbruch zwischen Key und Value oder einen Kommentar dazwischen, alles legale PDF-Syntax. Der Reader nimmt dann den Default true an, findet ein F in einer vollkommen gültigen /Perms, und verwirft die Datei. Eine gültige Datei, abgelehnt. Parsen Sie den Key und seinen Wert als Tokens, und behandeln Sie einen Nicht-Boolean-Wert als malformed, statt ihn zu defaulten

ISO/TS 32004 gibt Bit 13 einen Job: Ist es clear, trägt das Dokument einen PDF MAC, den Reader prüfen müssen. Das MAC-Token sitzt im /AuthCode-Dictionary des Trailers, das nicht verschlüsselt ist, also schließt eine Prüfung, die nur nach /AuthCode sucht, dass eine Datei ohne es nie Integritätsschutz hatte. Auch /KDFSalt mitzulöschen versteckt die Anforderung nicht, denn das gelöschte Bit wohnt in /P, und /P wird durch /Perms authentifiziert. PDFium Components AESV4-Entschlüsselung behandelt deshalb ein cleares Bit 13 oder ein übriggebliebenes /KDFSalt ohne /AuthCode als einen gestrippten MAC und scheitert, und wenn der Integritätsschutz aus ist, setzt der Writer Bit 13, sodass sein Output nie einen MAC ankündigt, den er nicht hat. Die Key-Hierarchie und die Byte-Range hinter diesem Token gehören zu AES-GCM-Verschlüsselung und dem ISO/TS 32004 MAC in PDF 2.0, und der iterierte Passwort-Hash, der den File-Key freischaltet, bevor irgendetwas davon läuft, ist unter dem R6-Passwort-Hash und seiner Rundenzahl behandelt

Permissions oder UserPermissions: was soll Ihre UI lesen?

Lesen Sie Permissions, um zu entscheiden, was die aktuelle Session darf, und UserPermissions, um zu melden, was die Datei einem gewöhnlichen Empfänger gewährt. TPdf.Permissions wickelt PDFiums FPDF_GetDocPermissions und beschreibt den Zugriff, der diesem offenen Dokument gewährt wurde, was vom Passwort abhängt, das es freigeschaltet hat. TPdf.UserPermissions wickelt FPDF_GetDocUserPermissions und meldet weiterhin die User-Access-Gewährung aus /P, ganz gleich, welches Passwort benutzt wurde, also können die beiden auseinanderlaufen, wenn das Owner-Passwort die Datei öffnete. Beide sind LongWord; GrantedPermissions und UserGrantedPermissions dekodieren sie in TPdfPermissions-Sets, und HasPermission und UserHasPermission prüfen ein einzelnes Mitglied. SecurityHandlerRevision sagt Ihnen, ob die Datei überhaupt einen Security Handler hat. Die Routine unten druckt beide Integer-Sichten des rohen Worts neben dem dekodierten Set, ein schneller Weg zu sehen, warum eine Datei "nicht drucken will"

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;   // vor Active setzen
    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;

// Erwartet für die vorhin geschriebene Datei:
//   R6  /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
//   permPrint True, permModify False, permCopy False, ...

Die Durchsetzung gehört der Host-Anwendung

PDFium rendert, extrahiert Text und rastert Seiten, was auch immer die Flags sagen. Die Engine meldet Permissions und überlässt die Policy dem Caller. Wer das User-Passwort hält, hält den entschlüsselten Inhalt — bei leerem User-Passwort ist das jeder — also kann Vertraulichkeit nur von einem User-Passwort kommen, das der Reader nicht hat. Legen Sie die Prüfung in jeden Command-Handler (Kopieren, Export, Seitenlöschung, Druck), statt Toolbar-Buttons zu verstecken, denn Shortcuts, Kontextmenüs und Automation umgehen eine Toolbar alle, dieselbe Schichtung wie in einer sicheren PDF-Preview-Pane für Delphi-Apps. Ein Druckbefehl testet zum Beispiel permPrint, bevor er irgendetwas in die Warteschlange stellt

In einer AES-256-Datei beweist ein -1084 im Klartext nichts, bis die ersten vier entschlüsselten Bytes von /Perms C4 FB FF FF lauten. Dieser Byte-für-Byte-Vergleich läuft innerhalb des AESV4-, PDF-MAC- und verschlüsselten Inkrementell-Speichern-Codes der PDFium Component