HotPDF Delphi PDF component implementuje model crypt filterů podle ISO 32000-1 §7.6.5 jako tři nezávislé zásady namísto jednoho přepínače: ConfigureCryptFilterDefaults samostatně přiřadí string filter /StrF, stream filter /StmF a embedded-file filter /EFF, SetStreamCryptFilter přepíše jediný stream a GetLoadedCryptFilterInfo nahlásí, co deklaruje příchozí soubor. Většina interop chyb šifrovaných PDF žije v mezerách mezi těmito třemi cestami
Tady je selhání, které lidi do této vrstvy pošle. Tým vydá dokument, jehož obsah stránky musí zůstat čitelný downstream nástrojem, ale přiložený payload nesmí, takže nastaví /EFF /StdCF a ponechá /StmF /Identity. Acrobat ho otevře bez potíží. Konformní čtečka třetí strany vrátí přílohu jako ciphertextový odpad, protože /EFF je policy na straně producenta určující, který filter platí pro embedded files, zatímco obecná čtečka rozliší neoznačený stream přes /StmF. Opravou není jiná hodnota /EFF. Opravou je explicitní filter /Crypt přímo na embedded-file streamu
Co crypt filter vrstva skutečně řídí
Crypt filtery sedí mezi šifrovacím algoritmem a grafem objektů a rozhodují, kterých objektů se algoritmus dotkne, nikoli jak funguje. Dictionary /CF uvnitř encryption dictionary mapuje jména na definice filterů, z nichž každá nese metodu /CFM, volitelný /Length a /AuthEvent. Tři vrchní položky /StrF, /StmF a /EFF pak vybírají, který z pojmenovaných filterů platí pro stringy, pro streamy bez explicitního filtru a pro embedded files. HotPDF záměrně omezuje, co jeho vestavěné handlery zapíší. ConfigureCryptFilterDefaults přijímá pouze rezervovaná jména aktivního handleru: standard security handler vytvoří /StdCF nebo /Identity, public-key handler vytvoří /DefaultCryptFilter nebo /Identity a cokoli jiného vyvolá EArgumentException přímo v místě volání. Filtry zapsané externími producenty pod jinými názvy se při loadu, inspekci a compatibility rewrite stále zachovají, takže HotPDF je jako writer konzervativní a jako reader tolerantní. Platí ještě dvě pojistky: volání vyvolá EInvalidOpException, jakmile začne serializace dokumentu, a znovu, pokud je dokument v inkrementálním update, protože zásadu šifrování nelze mezi revizemi stejného souboru změnit
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;
// stringy šifrované, page streamy v plaintextu, přílohy šifrované
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;
Jedno omezení je dobré uvést hned, protože se kontroluje pozdě a lidi překvapí. Pojmenované crypt filtery v HotPDF vyžadují šifrování dokumentu aes128, aes256 nebo aesgcm. Nastavte policy filtru nad RC4 k40 nebo k128 a validační průchod, který běží při zapnutí šifrování, vyvolá chybu místo tichého povýšení typu klíče. Je to stejný přístup jako u zbytku cesty šifrování PDF AES-256 v Delphi: nejednoznačnou konfiguraci odmítnout místo hádání, co volající myslel
Proč položka /Length znamená dvě různé věci
Protože specifikace ji definuje ve dvou různých jednotkách podle security handleru a HotPDF musí respektovat obě. V dictionary crypt filtru, jehož /CFM je /V2, se /Length u standard security handleru vyjadřuje v bajtech a u public-key handleru v bitech. /Length v encryption dictionary vedle /V (ISO 32000-1 §7.6.2) je vždy v bitech. Přečtěte dictionary filtru s /Length 16 a v souboru se standard handlerem máte 128bitový klíč, zatímco v public-key souboru odmítnutý soubor. HotPDF to při zachycení načtené konfigurace normalizuje. Délku /V2 filtru /Length vynásobí osmi pouze tehdy, když soubor není šifrovaný public-key metodou, při chybějící vlastní hodnotě filtru použije /Length z úrovně dokumentu a výsledek uloží do THPDFCryptFilterInfo.KeyLengthBits. AESV2 je připnutý na 128 bitů a AESV3 a AESV4 na 256, protože tyto metody nemají vyjednatelnou délku klíče. Přísná část přichází potom: akceptováno je pouze 40 a 128 bitů u /V2. Filtr, který se vyřeší na jinou délku, se nahlásí jako nedostupný a operace selže, místo aby se zaokrouhlil na 128 s teorií, že většina producentů stejně myslela 128. Tichá normalizace délky klíče je cesta k souboru, který se dešifruje na vašem stroji a nikde jinde
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 a /StmF defaultují na Identity; /EFF defaultuje na /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;
Co zaručuje /CFM /None a jak se liší /Identity
Ke stejnému výsledku vedou různými cestami a jejich záměna rozbíjí lookupy. Pojmenovaný filter s /CFM /None i pojmenovaný filter, který /CFM úplně vynechá, znamenají, že tento filter nic nešifruje ani nedešifruje — HotPDF chybějící položku před resolvingem převede na None, takže oba skončí u hcfmNone s uloženou délkou klíče nula. /Identity je odlišný typ: jde o rezervované jméno, které lookup /CF úplně obchází, takže dokument může na /Identity odkazovat, aniž by ho kdekoli v /CF definoval. PDF names rozlišují velikost písmen, proto je nevyjednatelný ještě jeden detail implementace: žádný lookup crypt filtru nesmí být case-insensitive. HotPDF vyhledává názvy poddictionary /CF, položku /Length filtru i kontrolu /Type streamu case-sensitivně. Soubor, který definuje /stdcf, zatímco /StmF ukazuje na /StdCF, je malformed a považovat obě klíče za stejné by změnilo odhalitelnou chybu autora v tichou aplikaci špatného klíče na každý stream dokumentu
Jak zajistit, aby /EFF platil pro embedded-file streamy
Když se /EFF liší od /StmF, embedded-file stream potřebuje explicitní úvodní položku /Crypt ve svém /Filter a odpovídající dictionary /DecodeParms s /Name na stejné pozici v poli. HotPDF to řeší pro každý stream při ukládání: zjistí /Type /EmbeddedFile, zdědí nastavený embedded-file filter a explicitní marker /Crypt vypíše jen tehdy, když se zděděné jméno liší od efektivního výchozího filtru streamu. Když /EFF a /StmF souhlasí, marker se nezapisuje, protože reader by stejně vyřešil stejný filter. Na pozici v poli pak záleží stejně jako na jménu. Když HotPDF stream načítá zpět, projde /Filter a hledá položku /Crypt, uloží její index a potom na stejném indexu v poli /DecodeParms vyhledá /Name. /Crypt na indexu 0 spárovaný s parametry na indexu 1 vyřeší /Identity, ne váš filter. Proto writer také doplní pole parametrů nulovou hodnotou, pokud stream dříve měl /Filter, ale neměl /DecodeParms: pozice musí zůstat zarovnané
Pod tím je ještě ostřejší past. Když je existující /Filter nebo /DecodeParms nepřímý objekt — běžné u souborů z generátorů, které sdílejí jedno pole filterů mezi mnoha streamy — vložení /Crypt na místě by změnilo sdílený graf filterů a poškodilo každý další stream, který na něj ukazuje. HotPDF nepřímý objekt vyřeší a nejdříve ho zkopíruje do přímého objektu vlastněného konkrétním streamem, přičemž vyčistí čísla objektu a generace, takže původní nepřímý kořen se do nového pole nikdy nevloží. U streamu, který už používal ASCIIHexDecode, bude serializovaný výsledek /Filter [ /Crypt /ASCIIHexDecode ] s /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Stejná poziční disciplína řídí každý další řetězec filterů včetně těch, kterými procházíte při extrakci obrázků z načteného PDF přes jejich decode filtery
// Editor už drží načtený dokument a ContentStream je
// THPDFStreamObject, jehož /Filter je nepřímé jméno /ASCIIHexDecode
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');
// Prázdné jméno vyčistí override a při příštím uložení odstraní
// zastaralou položku /Crypt společně s jejími decode parametry
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Dědí object streamy politiku dokumentu /Encrypt
Ne a předpoklad, že ano, je spolehlivá cesta k vytvoření odpadu. Object stream musí dodržet skutečnou policy /StmF nebo vlastní explicitní marker /Crypt: samotná přítomnost dictionary /Encrypt neznamená, že každý kontejner /ObjStm obsahuje ciphertext. Dokument s /StmF /Identity má plaintextové object streamy, i když jeho stringy jsou plně šifrované, a decoder, který je přesto dešifruje, předá inflate fázi vstup, který nikdy nebyl výstupem deflate
Důsledek pro členské objekty stojí za druhé přečtení. Podle ISO 32000-1 §7.5.7 jsou stringy uvnitř šifrovaného object streamu po dešifrování kontejneru už plaintextové, takže další dešifrování by bylo double-decrypt. HotPDF tomu brání dotazem, zda byl kontejner každého objektu typu 2 šifrovaný, a pokud ano, objekt přeskočí a skipy počítá do XRefProbeDecryptObjStmSkips jako přímý důkaz, že pojistka zabrala. Když byl kontejner plaintextový, členské stringy nikdy nebyly ničím pokryté, takže HotPDF tyto členy materializuje a na každý jednotlivě aplikuje /StrF — s klíčem, který implementace skutečně používá, podle čísla a generace členského objektu, nikoli podle čísla obsahujícího objektu /ObjStm. Otočte to u souboru se smíšenou policy a každý string v každém komprimovaném objektu se dekóduje na šum. Pravidla na úrovni kontejneru jsou dále popsána v poznámkách o PDF object streamech a inkrementálních updatech
Kde HotPDF odmítá hádat
Sémantika crypt filterů neexistuje pod /V 4, takže HotPDF na takovém souboru odmítne override pro jednotlivý stream explicitní chybou místo zápisu markeru /Crypt, který by žádný konformní reader nerespektoval. Totéž platí při čtení: encryption dictionary s /V pod 4 vyčistí všechna tři načtená jména filterů, protože tam není co hlásit. Další tři hranice jsou vynuceny záměrně:
- Per-stream filter jiný než
Identityna public-key šifrovaném dokumentu se odmítá, protože stream-specific policy pod public-key handlerem potřebuje stream-specific recipient envelope, který HotPDF zatím negeneruje - Embedded files šifrované public-key metodou, jejichž
/EFFse liší od efektivního/StmF, se odmítají ze stejného důvodu místo zápisu tvaru, který nikdo nerozšifruje - Rychlá cesta AES-256 pro přímý soubor platí pouze tehdy, když stringy, streamy i embedded files vyřeší stejnou metodu crypt filtru a žádný objekt v souboru nemá explicitní
/Crypt; smíšená policy nebo plaintext metadata vynutí fallback na cestu přes celý graf objektů
Nic z toho nejsou výkonnostní rozhodnutí. Označují místa, kde chybný odhad vytvoří PDF, které se otevře v jednom vieweru, selže v jiném a vývojáři nedá žádný signál, dokud ho nenahlásí zákazník. Odmítnutí v ConfigureCryptFilterDefaults nebo při ukládání stojí jednu výjimku; potichu špatně zaklíčovaná příloha stojí celé support kolo. Pokud v Delphi nebo C++Builderu vytváříte nebo čtete šifrovaná PDF — selektivně plaintextový obsah stránek s šifrovanými přílohami, šifrované payload wrappery PDF 2.0 nebo interop se soubory, jejichž policy crypt filterů jste nevolili — popsané API crypt filterů je součástí aktuálního HotPDF Delphi PDF component vedle cest šifrování, object streamů a inkrementálních updatech, na nichž staví