Odborný článok

Kryptografické filtre PDF v Delphi: politiky StmF, StrF, EFF

HotPDF Delphi PDF component implementuje model crypt filterov ISO 32000-1 §7.6.5 ako tri nezávislé politiky namiesto jedného prepínača: ConfigureCryptFilterDefaults priraďuje filter stringov /StrF, filter streamov /StmF a filter vložených súborov /EFF oddelene, SetStreamCryptFilter prepíše jeden stream a GetLoadedCryptFilterInfo oznámi, čo deklaruje vstupný súbor. Väčšina problémov interoperabilty pri šifrovaných PDF žije v medzerách medzi týmito tromi nastaveniami

Tu je zlyhanie, ktoré ľudí pošle do tejto vrstvy. Tím dodá dokument, v ktorom má obsah stránky zostať čitateľný pre downstream nástroj, ale priložený payload nesmie, preto nastaví /EFF /StdCF a ponechá /StmF /Identity. Acrobat ho otvorí bez problémov. Konformná čítačka tretej strany vráti prílohu ako šifrovaný odpad, pretože /EFF je policy na strane producenta o tom, ktorý filter platí pre vložené súbory, zatiaľ čo všeobecná čítačka stále vyrieši neoznačený stream cez /StmF. Opravou nie je iná hodnota /EFF. Opravou je explicitný /Crypt filter priamo na streame vloženého súboru

Čo vrstva crypt filterov skutočne riadi

Crypt filtery ležia medzi šifrovacím algoritmom a objektovým grafom a rozhodujú, ktorých objektov sa algoritmus dotkne, nie ako funguje. Dictionary /CF vo vnútri encryption dictionary mapuje názvy na definície filterov, z ktorých každá nesie metódu /CFM, voliteľný /Length a /AuthEvent. Tri top-level položky /StrF, /StmF a /EFF potom vyberajú, ktorý pomenovaný filter platí pre stringy, pre streamy bez explicitného filtra a pre vložené súbory. HotPDF zámerne obmedzuje to, čo jeho vstavané handlery budú zapisovať. ConfigureCryptFilterDefaults prijíma iba rezervované názvy aktívneho handlera: Standard security handler emituje /StdCF alebo /Identity, public-key handler emituje /DefaultCryptFilter alebo /Identity a čokoľvek iné vyvolá EArgumentException priamo na call site. Filtery zapísané externými producentmi pod inými názvami sa pri load, inspection a compatibility rewrite cestách stále zachovajú, takže HotPDF je ako writer konzervatívny a ako reader zhovievavý. Platia ešte dve ochrany: volanie vyvolá EInvalidOpException, keď sa už začala serializácia dokumentu, a znova, ak je dokument v inkrementálnej aktualizácii, pretože encryption policy sa medzi revíziami toho istého súboru nemôže meniť

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;
    // šifrované stringy, plaintext page streams, šifrované prílohy
    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 obmedzenie sa oplatí uviesť hneď na začiatku, pretože sa kontroluje neskoro a ľudí prekvapí. Pomenované crypt filtery v HotPDF vyžadujú šifrovanie dokumentu aes128, aes256 alebo aesgcm. Nakonfigurujte filter policy nad RC4 k40 alebo k128 a validačný prechod, ktorý sa spustí pri zapnutí šifrovania, vyvolá chybu namiesto tichého povýšenia typu kľúča. Je to rovnaký postoj ako pri zvyšku cesty šifrovania PDF AES-256 v Delphi: nejednoznačnú konfiguráciu odmietnuť namiesto hádania, čo volajúci myslel

Prečo znamená položka /Length dve rôzne veci?

Pretože špecifikácia ju definuje v dvoch rôznych jednotkách podľa security handlera a HotPDF musí rešpektovať obe. V crypt filter dictionary, ktorého /CFM je /V2, je položka /Length vyjadrená v bajtoch pod Standard security handlerom a v bitoch pod public-key handlerom. /Length v encryption dictionary, ktorý sedí vedľa /V (ISO 32000-1 §7.6.2), je vždy v bitoch. Prečítajte filter dictionary s /Length 16 a v súbore so Standard handlerom máte 128-bitový kľúč, zatiaľ čo v public-key súbore odmietnutý súbor. HotPDF to normalizuje pri zachytení načítanej konfigurácie. /Length filtra /V2 násobí ôsmimi iba vtedy, keď súbor nie je šifrovaný public-key, pri vynechaní vlastnej hodnoty filtra použije document-level /Length a výsledok uloží do THPDFCryptFilterInfo.KeyLengthBits. AESV2 je pripnutý na 128 bitov a AESV3 aj AESV4 na 256, pretože tieto metódy nemajú vyjednateľnú veľkosť kľúča. Prísna časť prichádza potom: prijímajú sa iba 40-bitové a 128-bitové /V2. Filter, ktorý sa vyrieši na inú dĺžku, sa nahlási ako nedostupný a operácia zlyhá namiesto zaokrúhlenia na 128 s teóriou, že väčšina producentov aj tak myslela 128. Tichá normalizácia dĺžky kľúča je spôsob, ako dodáte súbor, ktorý sa dešifruje na vašom počítači a nikde inde

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 predvolene smerujú na Identity; /EFF predvolene 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;

Čo garantuje /CFM /None a v čom sa líši /Identity?

K rovnakému výsledku sa dostávajú rôznymi cestami a ich zamieňanie rozbije lookups. Pomenovaný filter, ktorého /CFM je /None, aj pomenovaný filter, ktorý /CFM úplne vynechá, znamenajú, že tento filter nevykonáva žiadne šifrovanie ani dešifrovanie — HotPDF chýbajúcu položku pred resolvingom namapuje na None, takže oba skončia na hcfmNone so zaznamenanou dĺžkou kľúča nula. /Identity je iný druh: ide o rezervovaný názov, ktorý obíde lookup /CF úplne, takže dokument môže odkazovať na /Identity bez toho, aby ho kdekoľvek v /CF definoval. PDF names rozlišujú veľké a malé písmená, preto je ďalší implementačný detail nevyjednateľný: žiadny lookup crypt filtra nesmie byť case-insensitive. HotPDF resolveuje názvy pod-dictionary /CF, položku /Length filtra aj kontrolu streamového /Type cez case-sensitive dictionary lookups. Súbor, ktorý definuje /stdcf, zatiaľ čo /StmF smeruje na /StdCF, je chybný a považovať tieto dva kľúče za rovnaké by zmenilo detekovateľnú chybu authoringu na nesprávny kľúč potichu aplikovaný na každý stream dokumentu

Ako udržať /EFF na streame vloženého súboru

Keď sa /EFF líši od /StmF, stream vloženého súboru potrebuje explicitnú úvodnú položku /Crypt vo svojom /Filter a zodpovedajúci dictionary /DecodeParms s /Name na tej istej pozícii v poli. HotPDF to vyrieši pre každý stream pri save: zistí /Type /EmbeddedFile, zdedí nakonfigurovaný filter vložených súborov a explicitnú značku /Crypt emituje iba vtedy, keď sa zdedený názov líši od efektívneho predvoleného filtra streamu. Keď /EFF a /StmF súhlasia, značka sa nezapíše, pretože reader by aj tak vyriešil rovnaký filter. Na pozícii poľa potom záleží rovnako ako na názve. Keď HotPDF načíta stream späť, prehľadá /Filter po položke /Crypt, zaznamená jej index a potom vyhľadá ten istý index v poli /DecodeParms, aby našiel /Name. /Crypt na indexe 0 spárovaný s parametrami na indexe 1 sa vyrieši na /Identity, nie na váš filter. Aj preto writer doplní pole parametrov null hodnotou, keď stream predtým mal /Filter, ale nemal /DecodeParms: pozície musia zostať zarovnané

Pod tým je ostrejší háčik. Ak je existujúci /Filter alebo /DecodeParms nepriamy objekt — bežné pri súboroch z generátorov, ktoré zdieľajú jedno pole filterov medzi mnohými streamami — vloženie /Crypt na mieste by zmenilo zdieľaný filter graph a poškodilo každý ďalší stream, ktorý naň ukazuje. HotPDF nepriamy objekt resolveuje a najprv ho klonuje do priameho objektu vlastného pre stream, pričom vymaže čísla objektu a generácie, takže pôvodný nepriamy root sa nikdy nevloží dovnútra nového poľa. Pre stream, ktorý už používal ASCIIHexDecode, je serializovaný výsledok /Filter [ /Crypt /ASCIIHexDecode ] s /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Rovnaká pozičná disciplína riadi každý ďalší filter chain vrátane tých, ktorými prechádzate pri extrakcii obrázkov z načítaného PDF cez ich decode filtery

// Editor už drží načítaný dokument a ContentStream je
// THPDFStreamObject, ktorého /Filter je nepriamy názov /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ázdny názov vymaže override a pri ďalšom save odstráni zastaraný /Crypt
// entry spolu s jeho decode parametrami
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');

Dedia object streams policy dokumentu /Encrypt?

Nie, a predpoklad, že ju dedia, je spoľahlivý spôsob výroby odpadu. Object stream musí nasledovať skutočnú policy /StmF alebo vlastnú explicitnú značku /Crypt: samotná prítomnosť dictionary /Encrypt neznamená, že každý kontajner /ObjStm je ciphertext. Dokument s /StmF /Identity má plaintext object streams, hoci jeho stringy sú úplne šifrované, a decoder, ktorý ich dešifruje aj tak, odovzdá inflate fáze vstup, ktorý nikdy nebol deflate output

Dôsledok pre členské objekty je časť, ktorú sa oplatí prečítať dvakrát. Podľa ISO 32000-1 §7.5.7 sú stringy vo vnútri šifrovaného object streamu už plaintextom po dešifrovaní samotného kontajnera, takže ich ďalšie dešifrovanie by bolo double-decrypt. HotPDF to chráni tak, že zisťuje, či bol kontajner každého objektu typu 2 šifrovaný, a ak áno, objekt preskočí; preskočenia započítava do XRefProbeDecryptObjStmSkips ako priamy dôkaz, že ochrana zabrala. Keď bol kontajner plaintext, členské stringy neboli pokryté ničím, takže HotPDF tieto členy materializuje a na každý zvlášť aplikuje /StrF — kľúčovaný, ako to implementácia skutočne robí, číslom objektu a generáciou člena, nie číslom obsahujúceho objektu /ObjStm. Otočte toto na súbore so zmiešanou policy a každý string v každom komprimovanom objekte sa dekóduje na šum. Pravidlá na úrovni kontajnera sú ďalej pokryté v poznámkach o PDF object streamoch a inkrementálnych aktualizáciách

Kde HotPDF odmieta hádať

Sémantika crypt filterov neexistuje pod /V 4, preto HotPDF odmietne akýkoľvek per-stream override na takom súbore explicitnou chybou namiesto zápisu značky /Crypt, ktorú by žiadny konformný reader nerešpektoval. To isté platí pri čítaní: encryption dictionary s /V pod 4 vymaže všetky tri načítané názvy filterov, pretože tam nie je čo hlásiť. Ďalšie tri hranice sa vynucujú zámerne:

  • Per-stream filter odlišný od Identity na public-key šifrovanom dokumente sa odmietne, pretože stream-specific policy pod public-key handlerom potrebuje stream-specific recipient envelope, ktorý HotPDF zatiaľ neemituje
  • Public-key šifrované vložené súbory, ktorých /EFF sa líši od efektívneho /StmF, sa odmietnu z rovnakého dôvodu namiesto zápisu tvaru, ktorý by sa nedal dešifrovať nikomu
  • Priama fast path AES-256 pre súbor platí iba vtedy, keď stringy, streamy aj vložené súbory vyriešia rovnakú metódu crypt filtra a žiadny objekt v súbore nenesie explicitný /Crypt; zmiešaná policy alebo plaintext metadata vynútia fallback na úplnú cestu objektovým grafom

Nič z toho nie sú výkonnostné rozhodnutia. Označujú miesta, kde nesprávny odhad vytvorí PDF, ktoré sa otvorí v jednom vieweri, zlyhá v inom a vývojárovi nedá vôbec žiadny signál, kým to nenahlási zákazník. Odmietnutie v ConfigureCryptFilterDefaults alebo pri save stojí jednu výnimku; potichu nesprávne zakľúčovaný vložený súbor stojí celý support cycle. Ak tvoríte v Delphi alebo C++Builderi software, ktorý vyrába alebo spotrebúva šifrované PDF — selektívne plaintextový obsah stránok so šifrovanými prílohami, šifrované payload wrappers PDF 2.0 alebo interoperabilitu so súbormi, ktorých policy crypt filterov ste nevyberali — API crypt filterov opísané tu je súčasťou aktuálneho HotPDF Delphi PDF component spolu s cestami šifrovania, object streamov a inkrementálnych aktualizácií, na ktorých stavia