Articolo tecnico

Flag di permesso PDF, l'intero /P e la copia /Perms

Cifrate un report con una maschera pensata per permettere tutto, $FFFFFFF0, e la scheda Sicurezza di Acrobat elenca stampa e modifica del documento come non consentite. /P è una parola di flag a 32 bit senza segno numerata dal bit 1, quindi quella maschera cancella print (bit 3) e modify (bit 4); il valore tutto-consentito è $FFFFFFFC, il punto di partenza di TPdfEncryptOptions in PDFium Component

Com'è disposta la parola di flag /P

Solo otto posizioni portano significato, il resto è riservato, e il conteggio parte da 1. ISO 32000-1 §7.6.3.2 dice che P "shall be interpreted as an unsigned 32-bit quantity", numerata da 1 all'estremità bassa fino a 32 all'estremità alta, e dice ai reader conformi di ignorare ogni bit diverso da 3, 4, 5, 6, 9, 10, 11 e 12. ISO 32000-2 §7.6.4.2 corregge la Tabella 22 e precisa le posizioni riservate: i bit 1–2 devono essere 0, i bit 7–8 devono essere 1, e per la revision 3 e successive i bit 13–32 devono essere 1. Il PDF 2.0 ritira anche il bit 10, che i reader ora ignorano e i writer impostano sempre a 1 per i reader più vecchi; la deroga per l'accessibilità si sposta al bit 5, quindi un reader dovrebbe comportarsi come se la copia fosse permessa quando il contenuto va a tecnologia assistiva. Una formula copre ogni riga qui sotto: la maschera del bit n è 1 shl (n - 1) — e TPdfPermission si mappa sulla parola allo stesso modo

BitMascheraSignificato (revision 3 e successive)TPdfPermission
1–2$00000003Riservato, deve essere 0nessuno
3$00000004Stampa, a una qualità che dipende dal bit 12permPrint
4$00000008Modificare i contenuti con operazioni diverse da quelle dei bit 6, 9 e 11permModify
5$00000010Copiare o estrarre testo e graficapermCopy
6$00000020Aggiungere o modificare annotazioni, riempire campi; col bit 4, creare campipermAnnotate
7–8$000000C0Riservato, deve essere 1nessuno
9$00000100Riempire campi modulo esistenti, inclusi i campi firma, anche se il bit 6 è a zeropermFillForms
10$00000200Estrazione per accessibilità; inutilizzato nel PDF 2.0, sempre scritto come 1permAccessibility
11$00000400Assemblare: inserire, ruotare o eliminare pagine, creare voci di outline e miniaturepermAssemble
12$00000800Stampa con una rappresentazione fedele e di alta qualitàpermPrintHighQuality
13–32$FFFFF000Riservato, deve essere 1 (ISO/TS 32004 assegna il bit 13, vedere sotto)nessuno

La maschera $FFFFFFF0 nasce dal leggere "i bit bassi sono riservati" e cancellare un nibble intero per prudenza. La regione riservata erano solo i bit 1–2, maschera $3: cancellare $F cancella anche $4 e $8, che sono print e modify, e qualunque viewer che onori le flag rifiuterà entrambe. L'errore speculare è assemblare la maschera dal basso partendo da zero: mettete in OR $4, $10 e $800 e vi ritrovate una /P piccola e positiva con i bit 7–8 e 13–32 a zero. La maggior parte dei reader apre quel file senza commenti, perché la specifica li manda a ignorare i bit riservati, ma il valore è non conforme per la revision 3 e successive e un controllore severo è in diritto di lamentarsi. Una terza variante compare nel codice che conta i bit da zero, dove "bit 3" diventa $8 e il programma concede modify quando intendeva print. Nessuna di queste produce un errore al momento del save: i permessi semplicemente escono diversi da quelli che l'autore intendeva

Layout della parola di flag di permesso /P del PDF per PDFium Component in Delphi: bit riservati 1-2, 7-8 e 13-32, i bit TPdfPermission come permPrint e permModify, e le maschere $FFFFFFFC e $FFFFFFF0
Cancellare l'intero nibble basso con $FFFFFFF0 toglie anche print e modify, mentre costruire la maschera da zero lascia a zero i bit riservati della revision 3 e successive

/P -4 è lo stesso valore di /P 4294967292?

Sì. Stessi 32 bit, due forme di scrittura. Gli oggetti interi del PDF sono con segno, e la nota subito sopra la Tabella 22 osserva che, siccome i bit alti riservati sono 1, l'intero viene scritto come numero negativo; -4 è il valore tutto-consentito convenzionale per la revision 3 e successive. I producer che tengono il valore in una variabile senza segno scrivono invece 4294967292, ed entrambe le forme compaiono in file veri. La specifica stabilisce quale lettura sia giusta nella propria derivazione della chiave: il passo (d) dell'Algoritmo 2 in ISO 32000-1 §7.6.3.3 converte P in un numero binario senza segno a 32 bit e passa i quattro byte a MD5 col byte basso per primo, quindi per le revision 2-4 i valori -3904 e 4294963392 producono la stessa file key. Un parser che conserva /P in un Integer a 32 bit con segno respinge la forma senza segno come overflow; passate la variabile a Cardinal e a fallire è la forma negativa, mentre codice che confronta il testo del token con un valore conservato finisce per trattare un documento come due. Leggete il token in un Int64, accettate qualsiasi cosa da Low(Integer) a High(Cardinal), e mascherate a 32 bit prima di qualunque confronto. Il codice di decifratura di PDFium Component legge /P così per le revision 2-7, e il suo writer emette la forma con segno che mostra la nota della specifica. Il report dei bit riservati nello snippet si ferma al bit 14, dato che un bit 13 a zero è il modo in cui ISO/TS 32004 marca un file che porta un PDF MAC

uses
  System.SysUtils;

// Normalizzate il testo del token /P dal vostro parser di dizionario
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; ';
  // Il bit 13 ($1000) è escluso: ISO/TS 32004 lo azzera per richiedere un PDF MAC
  if (Mask and $FFFFE000) <> $FFFFE000 then
    Result := Result + 'bits 14-32 not all set; ';
end;

// Atteso: entrambe le chiamate danno $FFFFFFFC e un report dei bit riservati vuoto
//   TryNormalizePermissions('-4', M1);
//   TryNormalizePermissions('4294967292', M2);
Normalizzazione del valore /P del PDF in Delphi come fa PDFium Component: /P -4 e /P 4294967292 letti in Int64, verificati in intervallo da Low(Integer) a High(Cardinal) e mascherati al Cardinal $FFFFFFFC
Un parser Integer con segno e uno Cardinal respingono ciascuno una forma legale di /P, e leggere in Int64 poi mascherando a 32 bit riporta entrambe le forme a un valore solo

Costruire una maschera da un insieme TPdfPermissions

Partite con ogni bit riservato al suo valore richiesto e mettete in OR solo ciò che concedete, così un permesso dimenticato può solo diventare una negazione. TPdfEncryptOptions.Default nell'unità FPdfEncrypt contiene già $FFFFFFFC, che permette tutto, e cancellargli uno o due bit va bene per una restrizione rapida. Una policy che elenca cosa possono fare i destinatari si legge meglio come insieme, e l'array di costanti qui sotto si allinea con le posizioni dei bit che TPdf usa per decodificare la parola. Forzate il bit 10 a 1 a prescindere da ciò che passa il chiamante: ISO 32000-2 dice ai writer di impostarlo sempre, i reader PDF 2.0 lo ignorano comunque, e lasciarlo a zero rischia solo di bloccare i screen reader nel software più vecchio

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

  // Bit 7-8 e 13-32 a 1; bit 1-2 e ogni bit di permesso a 0
  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;   // qui Revision = erR5
    Opts.UserPassword := '';              // chiunque può aprire il file
    Opts.OwnerPassword := OwnerPassword;  // custodisce l'insieme di permessi
    Opts.Revision := erR6;
    Opts.Permissions := PermissionsToMask(
      [permPrint, permPrintHighQuality, permFillForms]);

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

Atteso: il dizionario Encrypt dell'output porta /P -1084, e un viewer che onori le flag offre la stampa a piena qualità e il riempimento dei form lasciando grigie le modifiche al contenuto, la copia, i commenti e l'assemblaggio delle pagine. Il bit 13 è una posizione che il writer gestisce per voi: con EnableIntegrityProtection lasciato a False, SaveAsEncrypted imposta il bit 13 a prescindere da ciò che dice la vostra maschera; con esso a True, il writer azzera il bit 13 sia in /P che in /Perms. Gli altri bit riservati restano responsabilità vostra — da qui il punto di partenza $FFFFF0C0. L'esempio sceglie erR6 perché R6 è la revision AES-256 che ISO 32000-2 standardizza; il record di default porta erR5

/Perms, la copia cifrata di /P nei file AES-256

Nelle revision 2-4 la parola di permessi è saldata alla chiave: cambiate un bit di /P e cambia l'input MD5, la chiave derivata è sbagliata, e non si decifra nulla. I file AES-256 rompono quel legame. L'Algoritmo 2.A in ISO 32000-2 §7.6.4.3.3 svelupa la chiave di cifratura del file da /UE o /OE, e P non gioca alcun ruolo in quella derivazione, quindi di per sé una /P in chiaro potrebbe essere modificata liberamente. Il /Perms chiude il varco. L'Algoritmo 10 (§7.6.4.4.9) riempie un blocco da 16 byte e lo cifra con la file key usando AES-256 in modalità ECB:

  • byte 0–3: la parola di permessi, byte basso per primo, estesa a 64 bit dai byte 4–7, tutti $FF
  • byte 8: ASCII T o F, in accordo con /EncryptMetadata
  • byte 9–11: il marcatore ASCII adb
  • byte 12–15: dati casuali che i reader ignorano

Il passo (f) dell'Algoritmo 2.A decifra il blocco, verifica adb ed esige che i byte 0–3 corrispondano a P — shall, non should. L'Algoritmo 13 (§7.6.4.4.12) ripete i controlli di marcatore e permessi col should più mite e ne aggiunge uno: il byte 8 contro EncryptMetadata. PDFium Component lo legge in modo severo ovunque il proprio codice apra il livello di cifratura: decifratura AESV4, validazione del PDF MAC e save incrementali in file cifrati. Dopo che la password utente o owner ha sbloccato la file key, quel codice confronta tutti e quattro i byte di permesso con la /P normalizzata, controlla il riempimento $FF, il byte T/F e il marcatore, e fa fallire il caricamento con "PDF encryption permissions validation failed" alla minima differenza, dato che una mancata corrispondenza significa che qualcuno ha modificato il dizionario dopo la cifratura e nessuna delle due copie può essere presa per buona

Blocco /Perms AES-256 in PDFium Component: la parola di permessi /P col byte basso per primo, riempimento $FF, il byte T o F di /EncryptMetadata e il marcatore adb, cifrato dall'Algoritmo 10 e verificato dagli Algoritmi 2.A e 13
Siccome la file key AES-256 non dipende più da /P, i byte /Perms decifrati sono ciò che prova che la parola di permessi in chiaro non è stata modificata dopo la cifratura

Il confronto su EncryptMetadata merita attenzione in qualunque reader scriviate. Una scorciatoia tipo Pos('/EncryptMetadata false', Dict) > 0 non vede /EncryptMetadata false con due spazi, un'interruzione di riga fra chiave e valore, o un commento in mezzo, tutta sintassi PDF legale. Il reader allora assume il default true, trova una F in un /Perms perfettamente valido, e respinge il file. Un file valido, rifiutato. Fate il parse della chiave e del suo valore come token, e trattate un valore non booleano come malformato invece di assegnarogli il default

ISO/TS 32004 dà al bit 13 un compito: quando è a zero, il documento porta un PDF MAC che i reader devono controllare. Il token MAC sta nel dizionario /AuthCode del trailer, che non è cifrato, quindi un controllo che cerca solo /AuthCode conclude che un file da cui è stato eliminato non è mai stato protetto nell'integrità. Eliminare anche /KDFSalt non nasconde il requisito, perché il bit azzerato vive in /P e la /P è autenticata dal /Perms. La decifratura AESV4 di PDFium Component tratta quindi un bit 13 a zero, o un /KDFSalt residuo, senza un /AuthCode come un MAC ripulito e fallisce, e quando la protezione dell'integrità è spenta il writer imposta il bit 13 così che il suo output non annunci mai un MAC che non ha. La gerarchia delle chiavi e il byte range dietro quel token appartengono a cifratura AES-GCM e MAC ISO/TS 32004 nel PDF 2.0, e l'hash iterato della password che sblocca la file key prima che tutto questo giri è coperto in l'hash della password R6 e il suo numero di round

Permissions o UserPermissions: quale dovrebbe leggere la vostra UI?

Leggete Permissions per decidere cosa può fare la sessione corrente, e UserPermissions per riferire cosa il file concede a un destinatario ordinario. TPdf.Permissions incapsula il FPDF_GetDocPermissions di PDFium e descrive l'accesso concesso a questo documento aperto, che dipende dalla password che lo ha sbloccato. TPdf.UserPermissions incapsula FPDF_GetDocUserPermissions e continua a riferire la concessione di accesso utente della /P a prescindere da quale password sia stata usata, così le due possono differire quando è la owner password ad aver aperto il file. Entrambe sono LongWord; GrantedPermissions e UserGrantedPermissions le decodificano in insiemi TPdfPermissions, e HasPermission e UserHasPermission testano un singolo membro. SecurityHandlerRevision dice se il file ha un security handler. La routine qui sotto stampa entrambe le viste intere della parola grezza accanto all'insieme decodificato, un modo rapido per capire perché un file "non stampa"

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;   // impostare prima di 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;

// Atteso per il file scritto prima:
//   R6  /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
//   permPrint True, permModify False, permCopy False, ...

L'applicazione della policy spetta all'applicazione ospite

PDFium renderizza, estrae testo e rasterizza pagine a prescindere da ciò che dicono le flag. Il motore riferisce i permessi e lascia la policy al chiamante. Chi tiene la user password tiene il contenuto decifrato — con una user password vuota, sono tutti — quindi la confidenzialità può venire solo da una user password che il reader non ha. Mettete il controllo dentro ogni gestore di comando (copia, export, eliminazione di pagine, stampa) invece di nascondere pulsanti nella toolbar, visto che scorciatoie, menu contestuali e automazione aggirano tutti una toolbar, lo stesso livello usato in un pannello di anteprima PDF sicuro per app Delphi. Un comando di stampa, per esempio, testa permPrint prima di mandare qualcosa in coda

In un file AES-256, un -1084 in chiaro non prova nulla finché i primi quattro byte decifrati del /Perms non leggono C4 FB FF FF. Quel confronto byte per byte gira dentro il codice AESV4, PDF MAC e save incrementale cifrato del PDFium Component