Il componente PDF Delphi HotPDF implementa il modello dei crypt filter ISO 32000-1 §7.6.5 come tre policy indipendenti invece che come un solo switch: ConfigureCryptFilterDefaults assegna separatamente il string filter /StrF, lo stream filter /StmF e l'embedded-file filter /EFF, SetStreamCryptFilter fa l'override di un singolo stream e GetLoadedCryptFilterInfo riporta ciò che dichiara un file in ingresso. La maggior parte dei bug di interoperabilità dei PDF cifrati vive negli spazi tra questi tre elementi
Ecco il guasto che porta a questo livello. Un team distribuisce un documento in cui il contenuto della pagina deve restare leggibile per uno strumento downstream ma il payload allegato no, quindi imposta /EFF /StdCF e lascia /StmF /Identity. Acrobat lo apre correttamente. Un reader di terze parti conforme restituisce l'allegato come spazzatura cifrata, perché /EFF è una policy lato producer su quale filter applicare agli embedded file e un reader generico risolve comunque uno stream non marcato tramite /StmF. La correzione non è un valore /EFF diverso. È un filtro /Crypt esplicito direttamente sull'embedded-file stream
Cosa controlla davvero il layer dei crypt filter
I crypt filter si collocano tra l'algoritmo di cifratura e l'object graph e decidono quali oggetti l'algoritmo tocca, non come funziona. Il dizionario /CF dentro l'encryption dictionary mappa i nomi alle definizioni dei filter, ciascuna con un metodo /CFM, un /Length opzionale e un /AuthEvent. Le tre entry di primo livello /StrF, /StmF ed /EFF selezionano poi quale dei filter nominati si applica alle stringhe, agli stream senza filter esplicito e agli embedded file. HotPDF limita deliberatamente ciò che i suoi handler integrati possono scrivere. ConfigureCryptFilterDefaults accetta solo i nomi riservati per l'handler attivo: lo Standard security handler emette /StdCF o /Identity, il public-key handler emette /DefaultCryptFilter o /Identity, e qualsiasi altro nome genera EArgumentException nel punto di chiamata. I filter scritti da producer esterni con altri nomi vengono comunque conservati nei percorsi di caricamento, ispezione e riscrittura compatibile, quindi HotPDF è conservativo come writer e permissivo come reader. Si applicano altre due guardie: la chiamata genera EInvalidOpException dopo l'inizio della serializzazione del documento e di nuovo se il documento è in un aggiornamento incrementale, perché la policy di cifratura non può cambiare tra revisioni dello stesso file
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'wrapper.pdf';
Pdf.OwnerPassword := 'owner-secret';
Pdf.UserPassword := 'open-secret';
Pdf.CryptKeyLength := aes128;
// stringhe cifrate, page stream in chiaro, allegati cifrati
Pdf.ConfigureCryptFilterDefaults('StdCF', 'Identity', 'StdCF');
Pdf.ActivateProtection := True;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 50, 0, 'Visible stream operators');
Pdf.AddDocumentAttachment('payload.bin', 'Encrypted payload');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Un vincolo va dichiarato subito, perché viene controllato tardi e sorprende. I crypt filter nominati in HotPDF richiedono cifratura del documento aes128, aes256 o aesgcm. Configurate una policy di filter sopra RC4 k40 o k128 e il passaggio di validazione eseguito quando la cifratura viene abilitata genera un errore invece di promuovere silenziosamente il tipo di chiave. È la stessa posizione progettuale del resto del percorso di cifratura PDF AES-256 in Delphi: rifiutare una configurazione ambigua invece di indovinare cosa intendesse il caller
Perché l'entry /Length significa due cose diverse?
Perché la specifica lo definisce in due unità diverse a seconda del security handler, e HotPDF deve rispettarle entrambe. In un crypt filter dictionary con /CFM uguale a /V2, l'entry /Length è espressa in byte nello Standard security handler e in bit nel public-key handler. La /Length dell'encryption dictionary, accanto a /V (ISO 32000-1 §7.6.2), è sempre in bit. Si legge un filter dictionary con /Length 16 e si ha una chiave da 128 bit in un file con Standard handler, ma un file rifiutato in uno public-key. HotPDF normalizza il dato quando cattura la configurazione caricata. Moltiplica per otto il /Length di un filter /V2 solo quando il file non è cifrato con public key, ricade sul /Length a livello documento quando il filter omette il proprio e memorizza il risultato in THPDFCryptFilterInfo.KeyLengthBits. AESV2 è fissato a 128 bit e AESV3 e AESV4 a 256, perché questi metodi non hanno una dimensione negoziabile. La parte rigida viene dopo: sono accettati solo /V2 da 40 e 128 bit. Un filter che si risolve in qualsiasi altra lunghezza viene segnalato come non disponibile e l'operazione fallisce, invece di essere arrotondato a 128 con l'idea che la maggior parte dei producer intendesse 128. Normalizzare silenziosamente la lunghezza di una chiave è il modo per distribuire un file che si decifra sulla vostra macchina e da nessun'altra parte
var
Reader: THotPDF;
Info: THPDFCryptFilterInfo;
I: Integer;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
if Reader.LoadFromFile('incoming.pdf', 'open-secret') <> 1 then
Exit;
// /StrF e /StmF hanno default Identity; /EFF ha default /StmF
WriteLn(Reader.LoadedStringCryptFilterName); // StdCF
WriteLn(Reader.LoadedStreamCryptFilterName); // Identity
WriteLn(Reader.LoadedEmbeddedFileCryptFilterName); // StdCF
for I := 0 to Reader.GetLoadedCryptFilterCount - 1 do
if Reader.GetLoadedCryptFilterInfo(I, Info) then
if (Info.Method = hcfmV2) and
not (Info.KeyLengthBits in [40, 128]) then
raise Exception.CreateFmt(
'crypt filter /%s: unsupported V2 key length %d',
[String(Info.Name), Info.KeyLengthBits]);
finally
Reader.Free;
end;
end;
Cosa garantisce /CFM /None e in cosa differisce /Identity?
Arrivano allo stesso risultato attraverso percorsi diversi, e confonderli rompe le lookup. Un filter nominato il cui /CFM è /None e un filter nominato che omette del tutto /CFM significano entrambi che quel filter non esegue cifratura o decifratura: HotPDF mappa l'entry mancante a None prima della risoluzione, quindi entrambi finiscono in hcfmNone con una key length registrata pari a zero. /Identity è diverso per natura: è il nome riservato che bypassa completamente la lookup di /CF, quindi un documento può referenziare /Identity senza definirlo in alcun punto di /CF. I PDF name sono case-sensitive, il che rende irrinunciabile un ulteriore dettaglio d'implementazione: nessuna lookup di crypt filter può essere case-insensitive. HotPDF risolve i nomi del sotto-dizionario /CF, l'entry /Length del filter e il controllo /Type dello stream tramite lookup case-sensitive. Un file che definisce /stdcf mentre /StmF punta a /StdCF è malformato, e trattare le due chiavi come uguali trasformerebbe un bug di authoring rilevabile in una chiave errata applicata silenziosamente a ogni stream del documento
Far rispettare /EFF sugli embedded-file stream
Quando /EFF differisce da /StmF, l'embedded-file stream ha bisogno di un'entry /Crypt esplicita all'inizio del proprio /Filter e di un dizionario /DecodeParms corrispondente con /Name nella stessa posizione dell'array. HotPDF lo calcola per stream al salvataggio: rileva /Type /EmbeddedFile, eredita il filter embedded-file configurato ed emette il marker /Crypt esplicito solo quando quel nome ereditato differisce dal default effettivo dello stream. Quando /EFF e /StmF coincidono, non viene scritto alcun marker, perché un reader risolverebbe comunque lo stesso filter. La posizione nell'array conta quanto il nome. Quando HotPDF rilegge uno stream, cerca in /Filter l'entry /Crypt, ne registra l'indice e cerca poi lo stesso indice nell'array /DecodeParms per trovare il /Name. Un /Crypt all'indice 0 abbinato a parametri all'indice 1 risolve in /Identity, non nel vostro filter. È anche per questo che il writer riempie l'array dei parametri con un null quando lo stream aveva già un /Filter ma nessun /DecodeParms: le posizioni devono restare allineate
Sotto c'è un tranello ancora più netto. Se /Filter o /DecodeParms esistenti sono un indirect object, cosa comune nei file prodotti da generatori che condividono lo stesso filter array tra molti stream, inserire /Crypt in-place modificherebbe un filter graph condiviso e corromperebbe ogni altro stream che lo punta. HotPDF risolve l'indirect object e lo clona prima in un direct object privato dello stream, azzerando i numeri di object e generation così la radice indirect originale non viene mai inserita nel nuovo array. Per uno stream che usava già ASCIIHexDecode, il risultato serializzato è /Filter [ /Crypt /ASCIIHexDecode ] con /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. La stessa disciplina posizionale governa ogni altra catena di filter, comprese quelle percorse durante l'estrazione delle immagini da un PDF caricato attraverso i loro decode filter
// L'editor contiene già un documento caricato e ContentStream è un
// THPDFStreamObject il cui /Filter è un nome /ASCIIHexDecode indirect
Editor.OwnerPassword := 'owner-secret';
Editor.UserPassword := 'open-secret';
Editor.CryptKeyLength := aes128;
Editor.ConfigureCryptFilterDefaults('StdCF', 'Identity');
Editor.SetStreamCryptFilter(ContentStream, 'StdCF');
Editor.ActivateProtection := True;
Editor.SaveLoadedDocument('out.pdf');
// Un nome vuoto cancella l'override e rimuove il vecchio /Crypt
// insieme ai suoi decode parameter al salvataggio successivo
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Gli object stream ereditano la policy /Encrypt del documento?
No, e supporlo è un modo affidabile per produrre spazzatura. Un object stream deve seguire la policy effettiva /StmF oppure il proprio marker /Crypt esplicito: la semplice presenza di un dizionario /Encrypt non rende ogni container /ObjStm ciphertext. Un documento con /StmF /Identity ha object stream in chiaro anche se le stringhe sono completamente cifrate, e un decoder che li decifra comunque passa allo stage inflate un input che non è mai stato prodotto da deflate
La conseguenza per gli oggetti membri è la parte da rileggere. Secondo ISO 32000-1 §7.5.7, le stringhe dentro un object stream cifrato sono già in chiaro una volta decifrato il container, quindi decifrarle di nuovo sarebbe un double-decrypt. HotPDF lo impedisce verificando se il container di ogni oggetto di tipo 2 era cifrato e saltando l'oggetto quando lo era, conteggiando gli skip in XRefProbeDecryptObjStmSkips come evidenza diretta che la guardia è scattata. Quando il container era in chiaro, le stringhe membro non erano coperte da nulla, quindi HotPDF materializza quei membri e applica /StrF a ciascuno individualmente, usando come chiave, come fa davvero l'implementazione, il numero e la generation dell'oggetto membro, non il numero dell'object /ObjStm contenitore. Invertite questa regola su un file a policy mista e ogni stringa di ogni object compresso diventa rumore. Le regole a livello container sono trattate più avanti nelle note su object stream PDF e aggiornamenti incrementali
Dove HotPDF rifiuta di indovinare
La semantica dei crypt filter non esiste sotto /V 4, quindi HotPDF rifiuta qualsiasi override per stream su un file del genere con un errore esplicito, invece di scrivere un marker /Crypt che nessun reader conforme onorerebbe. Lo stesso vale in lettura: un encryption dictionary con /V sotto 4 azzera tutti e tre i nomi dei filter caricati, perché non c'è nulla da riportare. Altri tre confini vengono applicati deliberatamente:
- Un filter per stream diverso da
Identitysu un documento cifrato con public key viene rifiutato, perché una policy specifica per stream nel public-key handler richiederebbe un recipient envelope specifico per stream che HotPDF non emette ancora - Gli embedded file cifrati con public key il cui
/EFFdifferisce dall'/StmFeffettivo vengono rifiutati per lo stesso motivo, invece di essere scritti in una forma che nessuno può decifrare - Il fast path AES-256 per file diretti si applica solo quando stringhe, stream ed embedded file risolvono tutti nello stesso metodo di crypt filter e nessun oggetto del file porta un
/Cryptesplicito; una policy mista o metadati in chiaro forza il fallback al percorso completo sull'object graph
Nessuno di questi è una decisione di performance. Sono i punti in cui un'ipotesi errata produce un PDF che si apre in un viewer, fallisce in un altro e non dà allo sviluppatore alcun segnale finché un cliente non lo segnala. Un rifiuto in ConfigureCryptFilterDefaults o al salvataggio costa un'eccezione; un embedded file cifrato con la chiave sbagliata costa un ciclo di supporto. Se costruite software Delphi o C++Builder che produce o consuma PDF cifrati, contenuti di pagina selettivamente in chiaro con allegati cifrati, payload wrapper cifrati PDF 2.0 o interoperabilità con file di cui non avete scelto le policy dei crypt filter, l'API dei crypt filter descritta qui è inclusa nell'attuale componente PDF Delphi HotPDF, insieme ai percorsi di cifratura, object stream e aggiornamento incrementale su cui si basa