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
| Bit | Maschera | Significato (revision 3 e successive) | TPdfPermission |
|---|---|---|---|
| 1–2 | $00000003 | Riservato, deve essere 0 | nessuno |
| 3 | $00000004 | Stampa, a una qualità che dipende dal bit 12 | permPrint |
| 4 | $00000008 | Modificare i contenuti con operazioni diverse da quelle dei bit 6, 9 e 11 | permModify |
| 5 | $00000010 | Copiare o estrarre testo e grafica | permCopy |
| 6 | $00000020 | Aggiungere o modificare annotazioni, riempire campi; col bit 4, creare campi | permAnnotate |
| 7–8 | $000000C0 | Riservato, deve essere 1 | nessuno |
| 9 | $00000100 | Riempire campi modulo esistenti, inclusi i campi firma, anche se il bit 6 è a zero | permFillForms |
| 10 | $00000200 | Estrazione per accessibilità; inutilizzato nel PDF 2.0, sempre scritto come 1 | permAccessibility |
| 11 | $00000400 | Assemblare: inserire, ruotare o eliminare pagine, creare voci di outline e miniature | permAssemble |
| 12 | $00000800 | Stampa con una rappresentazione fedele e di alta qualità | permPrintHighQuality |
| 13–32 | $FFFFF000 | Riservato, 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
/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);
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
ToF, 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
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