Technisch artikel

PDF-permissievlaggen, het /P-integer en de /Perms-kopie

U versleutelt een rapport met een masker dat alles zou moeten toestaan, $FFFFFFF0, en het Security-tabblad van Acrobat zet printen en het document wijzigen op niet toegestaan. /P is een unsigned 32-bit flag word genummerd vanaf bit 1, dus dat masker wist print (bit 3) en modify (bit 4); de alles-toestaan-waarde is $FFFFFFFC, het startpunt van TPdfEncryptOptions in PDFium Component

Hoe het /P-flag word is ingedeeld

Slechts acht posities dragen betekenis, de rest is gereserveerd, en het tellen begint bij 1. ISO 32000-1 §7.6.3.2 zegt dat P "shall be interpreted as an unsigned 32-bit quantity", genummerd van 1 aan de low-order-kant tot 32 aan de high-order-kant, en heet conforme readers elke bit dan 3, 4, 5, 6, 9, 10, 11 en 12 te negeren. ISO 32000-2 §7.6.4.2 corrigeert Tabel 22 en spelt de gereserveerde posities uit: bits 1–2 moeten 0 zijn, bits 7–8 moeten 1 zijn, en voor revision 3 en later moeten bits 13–32 1 zijn. PDF 2.0 schaft ook bit 10 af, die readers nu negeren en writers altijd op 1 zetten voor oudere readers; de toegankelijkheidsuitzondering schuift door naar bit 5, dus een reader hoort te handelen alsof kopiëren is toegestaan wanneer de inhoud naar assistieve technologie gaat. Eén formule dekt elke rij hieronder. Het masker voor bit n is 1 shl (n - 1) — en TPdfPermission landt op dezelfde manier op het word

BitMaskerBetekenis (revision 3 en later)TPdfPermission
1–2$00000003Gereserveerd, moet 0 zijnnone
3$00000004Printen, op een kwaliteit die van bit 12 afhangtpermPrint
4$00000008Inhoud wijzigen door andere operaties dan die van bits 6, 9 en 11permModify
5$00000010Tekst en graphics kopiëren of extraherenpermCopy
6$00000020Annotations toevoegen of wijzigen, velden vullen; met bit 4, velden makenpermAnnotate
7–8$000000C0Gereserveerd, moet 1 zijnnone
9$00000100Bestaande formuliervelden vullen, inclusief signature-velden, zelfs als bit 6 gewist ispermFillForms
10$00000200Toegankelijkheidsextractie; ongebruikt in PDF 2.0, altijd als 1 weggeschrevenpermAccessibility
11$00000400Assembleren: pagina's invoegen, roteren of verwijderen, outline-items en thumbnails makenpermAssemble
12$00000800Printen naar een getrouwe, hoogwaardige weergavepermPrintHighQuality
13–32$FFFFF000Gereserveerd, moet 1 zijn (ISO/TS 32004 kent bit 13 toe, zie hieronder)none

Het masker $FFFFFFF0 komt uit het lezen van "de lage bits zijn gereserveerd" en het wissen van een hele nibble om veilig te zijn. De gereserveerde regio was alleen bits 1–2, masker $3; het wissen van $F veegt ook $4 en $8 weg, dat zijn print en modify, en elke viewer die de vlaggen honoreert weigert beide. De spiegelbeeldfout is het masker vanaf nul opbouwen: OR samen $4, $10 en $800 en u krijgt een kleine positieve /P met bits 7–8 en 13–32 op nul. De meeste readers openen dat bestand zonder commentaar, want de specificatie heet ze gereserveerde bits te negeren, maar de waarde is nonconform voor revision 3 en later, en een strenge checker mag klagen. Een derde variant duikt op in code die bits vanaf nul telt, waar "bit 3" $8 wordt en het programma modify toekent terwijl het print bedoelde. Geen van deze levert een fout op bij het opslaan. De permissies komen er gewoon anders uit dan de auteur bedoelde

PDF /P-permissieflag word-layout voor PDFium Component in Delphi: gereserveerde bits 1-2, 7-8 en 13-32, de TPdfPermission-bits zoals permPrint en permModify, en de maskers $FFFFFFFC en $FFFFFFF0
De hele lage nibble wissen met $FFFFFFF0 haalt ook print en modify weg, terwijl het masker vanaf nul opbouwen de gereserveerde enen van revision 3 en later op nul laat

Is /P -4 dezelfde waarde als /P 4294967292?

Ja. Dezelfde 32 bits, twee spellingen. PDF-integer-objecten zijn signed, en de noot vlak boven Tabel 22 wijst erop dat omdat de gereserveerde high-order-bits 1 zijn, het integer als negatief getal wordt weggeschreven; -4 is de gebruikelijke alles-toestaan-waarde voor revision 3 en later. Producenten die de waarde in een unsigned variabele bewaren schrijven in plaats daarvan 4294967292, en beide spellingen duiken in echte bestanden op. De specificatie beslist in zijn eigen key-afleiding welke lezing juist is: stap (d) van Algorithm 2 in ISO 32000-1 §7.6.3.3 zet P om naar een 32-bit unsigned binair getal en geeft de vier bytes aan MD5, low-order byte eerst, dus voor revisions 2 tot 4 produceren de waarden -3904 en 4294963392 dezelfde file key. Een parser die /P in een signed 32-bit Integer bewaart wijst de unsigned spelling af als overflow; wissel de variabele naar Cardinal en de negatieve spelling faalt in plaats daarvan, terwijl code die de tokentekst tegen een opgeslagen waarde vergelijkt er op het eind toe komt één document als twee te behandelen. Lees het token in een Int64, accepteer alles van Low(Integer) tot High(Cardinal), en maskeer naar 32 bits vóór elke vergelijking. De eigen decryptiecode van PDFium Component leest /P zo voor revisions 2 tot en met 7, en zijn writer geeft de signed vorm uit die de noot van de specificatie toont. Het reserved-bit-rapport in het fragment stopt bij bit 14, want een gewiste bit 13 is hoe ISO/TS 32004 een bestand markeert dat een PDF MAC draagt

uses
  System.SysUtils;

// Normaliseer de /P-tokentekst uit uw eigen 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 weggelaten: ISO/TS 32004 wist hem om een PDF MAC te eisen
  if (Mask and $FFFFE000) <> $FFFFE000 then
    Result := Result + 'bits 14-32 not all set; ';
end;

// Verwacht: beide aanroepen leveren $FFFFFFFC en een leeg reserved-bit-rapport op
//   TryNormalizePermissions('-4', M1);
//   TryNormalizePermissions('4294967292', M2);
De PDF /P-waarde normaliseren in Delphi zoals PDFium Component doet: /P -4 en /P 4294967292 gelezen in Int64, bereikgecontroleerd van Low(Integer) tot High(Cardinal) en gemaskeerd naar de Cardinal $FFFFFFFC
Signed Integer- en Cardinal-parsers wijzen elk één legale spelling van /P af, en Int64 lezen en dan naar 32 bits maskeren brengt beide spellingen op één waarde

Een masker bouwen uit een TPdfPermissions-set

Begin met elke gereserveerde bit op zijn vereiste waarde en OR er alleen in wat u toekent, zodat een vergeten permissie slechts ooit in een weigering kan uitmonden. TPdfEncryptOptions.Default in de unit FPdfEncrypt bevat al $FFFFFFFC, wat alles toestaat, en daar één of twee bits afwerven werkt prima voor een snelle beperking. Een beleid dat oplijst wat ontvangers mogen leest beter als een set, en de constante array hieronder sluit aan op de bitposities die TPdf gebruikt om het word te decoderen. Forceer bit 10 aan, wat de aanroeper ook doorgeeft. ISO 32000-2 heet writers hem altijd te zetten, PDF 2.0-readers negeren hem toch al, en hem kaal laten riskeert alleen dat screen readers in oudere software geblokkeerd raken

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 en 13-32 gezet; bits 1-2 en elke permissiebit gewist
  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 staat hier op erR5
    Opts.UserPassword := '';              // iedereen mag het bestand openen
    Opts.OwnerPassword := OwnerPassword;  // bewaakt de permissieset
    Opts.Revision := erR6;
    Opts.Permissions := PermissionsToMask(
      [permPrint, permPrintHighQuality, permFillForms]);

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

Verwacht: de dictionary Encrypt van de uitvoer draagt /P -1084, en een viewer die de vlaggen honoreert biedt printen op volle kwaliteit en het vullen van formulieren aan terwijl inhoudswijzigingen, kopiëren, commentaar en pagina-assemblage grijs staan. Bit 13 is één positie die de writer voor u beheert. Met EnableIntegrityProtection op False gelaten zet SaveAsEncrypted bit 13 wat uw masker ook zegt; met hem op True wist de writer bit 13 in zowel /P als /Perms. De andere gereserveerde bits blijven uw verantwoordelijkheid — vandaar het startpunt $FFFFF0C0. Het voorbeeld kiest erR6 omdat R6 de AES-256-revision is die ISO 32000-2 standaardiseert; het default-record draagt erR5

/Perms, de versleutelde kopie van /P in AES-256-bestanden

In revisions 2 tot 4 zit het permissieword aan de key gelast: verander één bit van /P en de MD5-invoer verandert, de afgeleide key is fout, en niets ontsleutelt. AES-256-bestanden doorbreken die koppeling. Algorithm 2.A in ISO 32000-2 §7.6.4.3.3 pakt de file encryption key uit /UE of /OE uit, en P speelt geen rol in die afleiding, dus op zichzelf zou een plaintext /P vrij te bewerken zijn. /Perms sluit dat gat. Algorithm 10 (§7.6.4.4.9) vult een blok van 16 bytes en versleutelt het met de file key via AES-256 in ECB-modus:

  • bytes 0–3: het permissieword, low-order byte eerst, tot 64 bits uitgebreid door bytes 4–7, die allemaal $FF zijn
  • byte 8: ASCII T of F, passend bij /EncryptMetadata
  • bytes 9–11: de ASCII-marker adb
  • bytes 12–15: willekeurige data die readers negeren

Stap (f) van Algorithm 2.A ontsleutelt het blok, verifieert adb en eist dat bytes 0–3 matchen met P — shall, niet should. Algorithm 13 (§7.6.4.4.12) herhaalt de marker- en permissiecontroles met de mildere should en voegt er één toe: byte 8 tegen EncryptMetadata. PDFium Component leest het op de strenge manier waar zijn eigen code de versleutelingslaag opent: AESV4-decryptie, PDF MAC-validatie en incrementele opslagen in versleutelde bestanden. Nadat het user- of owner password de file key opent, vergelijkt die code alle vier de permissiebytes met de genormaliseerde /P, controleert de vulling $FF, de byte T/F en de marker, en faalt de load met "PDF encryption permissions validation failed" bij elk verschil, want een mismatch betekent dat iemand de dictionary ná de versleuteling heeft bewerkt en geen van beide kopieën voor zoetkoek kan worden genomen

AES-256 /Perms-blok in PDFium Component: het /P-permissieword low-order byte eerst, de vulling $FF, de /EncryptMetadata-byte T of F en de marker adb, versleuteld door Algorithm 10 en geverifieerd onder Algorithms 2.A en 13
Omdat de AES-256-file key niet langer van /P afhangt bewijzen de ontsleutelde /Perms-bytes dat het plaintext-permissieword niet ná de versleuteling werd bewerkt

De vergelijking EncryptMetadata verdient zorg in elke reader die u schrijft. Een shortcut als Pos('/EncryptMetadata false', Dict) > 0 mist /EncryptMetadata false met twee spaties, een regeleinde tussen key en waarde, of een commentaar ertussenin, allemaal legale PDF-syntaxis. De reader neemt dan de default true aan, vindt F in een volkomen geldige /Perms, en weigert het bestand. Een geldig bestand, geweigerd. Parse de key en zijn waarde als tokens, en behandel een niet-boolean waarde als misvormd in plaats van haar te defaulten

ISO/TS 32004 geeft bit 13 een taak: wanneer hij gewist is draagt het document een PDF MAC die readers moeten controleren. Het MAC-token zit in de dictionary /AuthCode van de trailer, die niet is versleuteld, dus een controle die alleen naar /AuthCode kijkt concludeert dat een bestand waarin hij is verwijderd nooit integriteitsbescherming heeft gehad. Ook /KDFSalt verwijderen verbergt de eis niet, want de gewiste bit woont in /P en /P wordt geauthenticeerd door /Perms. De AESV4-decryptie van PDFium Component behandelt daarom een gewiste bit 13, of een achtergebleven /KDFSalt, zonder een /AuthCode als een gestripte MAC en faalt, en wanneer integriteitsbescherming uit staat zet de writer bit 13 zodat zijn uitvoer nooit een MAC aankondigt die hij mist. De key-hiërarchie en de byte range achter dat token horen bij AES-GCM-versleuteling en de ISO/TS 32004 MAC in PDF 2.0, en de geïtereerde password hash die de file key opent voordat dit alles draait staat beschreven in de R6-password hash en zijn rondentelling

Permissions of UserPermissions: welke moet uw UI lezen?

Lees Permissions om te beslissen wat de huidige sessie mag doen, en UserPermissions om te melden wat het bestand een gewone ontvanger toekent. TPdf.Permissions wikkelt FPDF_GetDocPermissions van PDFium en beschrijft de toegang die dit geopende document kreeg, wat afhangt van het password dat hem opende. TPdf.UserPermissions wikkelt FPDF_GetDocUserPermissions en blijft de user-access-toekenning uit /P melden welke password er ook werd gebruikt, dus de twee kunnen verschillen wanneer het owner password het bestand opende. Beiden zijn LongWord; GrantedPermissions en UserGrantedPermissions decoderen hen naar sets TPdfPermissions, en HasPermission en UserHasPermission toetsen één lid. SecurityHandlerRevision vertelt of het bestand überhaupt een security handler heeft. De routine hieronder drukt beide integer-weergaven van het rauwe word af naast de gedecodeerde set, een snelle manier om te zien waarom een bestand "niet wil printen"

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;   // zetten vóór 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;

// Verwacht voor het bestand dat eerder werd weggeschreven:
//   R6  /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
//   permPrint True, permModify False, permCopy False, ...

Afdwingen is werk voor de host-applicatie

PDFium rendert, extraheert tekst en rastert pagina's wat de vlaggen ook zeggen. De engine meldt permissies en laat het beleid aan de aanroeper. Wie het user password heeft heeft de ontsleutelde inhoud — met een leeg user password is dat iedereen — dus vertrouwelijkheid kan alleen komen uit een user password dat de reader mist. Zet de controle in elke command-handler (kopiëren, exporteren, pagina's verwijderen, printen) in plaats van toolbar-knoppen te verbergen, want sneltoetsen, contextmenu's en automatisering omzeilen allemaal een toolbar, dezelfde gelaagdheid als in een veilig PDF-previewpaneel voor Delphi-apps. Een print-commando toetst bijvoorbeeld permPrint voordat hij ook maar iets spoolt

In een AES-256-bestand bewijst -1084 in platte tekst niets totdat de eerste vier ontsleutelde bytes van /Perms C4 FB FF FF lezen. Die byte-voor-byte-vergelijking draait binnen de AESV4-, PDF MAC- en versleutelde incrementele-opslag-code van PDFium Component