Műszaki cikk

PDF crypt filter szabályok Delphiben: StmF, StrF, EFF

A HotPDF Delphi PDF component az ISO 32000-1 §7.6.5 crypt filter modelljét három független szabályként valósítja meg, nem egyetlen kapcsolóként: a ConfigureCryptFilterDefaults külön rendeli hozzá a string filtert, a /StrF-et, a stream filtert, a /StmF-et és a beágyazottfájl-filtert, az /EFF-et, a SetStreamCryptFilter egyetlen streamet ír felül, a GetLoadedCryptFilterInfo pedig jelenti, mit deklarál a bejövő fájl. A titkosított PDF-ek interoperabilitási hibáinak nagy része e három réteg közötti résekben él

Íme a hiba, amely erre a rétegre tereli az embert. Egy csapat olyan dokumentumot küld ki, amelyben az oldal tartalmának olvashatónak kell maradnia egy downstream eszköz számára, a csatolt payloadnak viszont nem, ezért beállítja az /EFF /StdCF-et, az /StmF /Identity-t pedig meghagyja. Az Acrobat rendben megnyitja. Egy szabványkövető külső reader a mellékletet titkosított szemétként adja vissza, mert az /EFF gyártói oldali szabály arról szól, melyik filter vonatkozik a beágyazott fájlokra, egy általános reader pedig továbbra is az /StmF-en keresztül old fel egy jelöletlen streamet. A javítás nem egy másik /EFF érték. A javítás magán a beágyazottfájl-streamen elhelyezett explicit /Crypt filter

Mit szabályoz valójában a crypt filter réteg?

A crypt filterek a titkosítási algoritmus és az objektumgráf között ülnek, és azt döntik el, melyik objektumot érinti az algoritmus, nem azt, hogyan működik. Az encryption dictionaryban lévő /CF dictionary neveket rendel filterdefiníciókhoz, amelyek mindegyike /CFM metódust, opcionális /Length-et és /AuthEvent-et hordoz. A három felső szintű bejegyzés, a /StrF, /StmF és /EFF ezután kiválasztja, melyik névvel jelölt filter vonatkozik a stringekre, az explicit filter nélküli streamekre és a beágyazott fájlokra. A HotPDF szándékosan korlátozza, mit hajlandó írni a beépített handler. A ConfigureCryptFilterDefaults csak az aktív handler számára fenntartott neveket fogadja el: a Standard security handler /StdCF-et vagy /Identity-t ír, a public-key handler /DefaultCryptFilter-t vagy /Identity-t, minden más pedig EArgumentException-t dob a hívás helyén. A külső producer által más néven írt filtereket betöltéskor, vizsgálatkor és kompatibilitási újraíráskor továbbra is megőrzi, tehát a HotPDF íróként konzervatív, olvasóként engedékeny. Két további őr is van: a hívás EInvalidOpException-t emel, ha a dokumentum szerializálása már elkezdődött, és akkor is, ha a dokumentum inkrementális frissítésben van, mert ugyanazon fájl revíziói között nem változhat a titkosítási szabály

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;
    // stringek titkosítva, page streamek plaintextként, mellékletek titkosítva
    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;

Egy korlátot érdemes rögtön kimondani, mert későn ellenőrzi a kód, és meglepi az embert. A HotPDF névvel ellátott crypt filterei aes128, aes256 vagy aesgcm dokumentumtitkosítást igényelnek. Ha RC4 k40 vagy k128 fölé konfigurálsz filter-szabályt, a titkosítás engedélyezésekor futó validáció kivételt emel, nem pedig csendben kulcstípust vált. Ez ugyanaz a hozzáállás, mint a Delphiben végzett AES-256 PDF-titkosítási útvonalnál: az egyértelműtlen konfigurációt utasítsd el, ne találgasd, mire gondolt a hívó

Miért jelent az /Length bejegyzés két különböző dolgot?

Azért, mert a szabvány a security handlertől függően két különböző mértékegységben definiálja, a HotPDF-nek pedig mindkettőt tiszteletben kell tartania. Egy olyan crypt filter dictionaryban, amelynek /CFM értéke /V2, a /Length a Standard security handler alatt bájtban, public-key handler alatt pedig bitben értendő. A /V mellett álló, encryption-dictionary szintű /Length (ISO 32000-1 §7.6.2) mindig bitben van. Ha egy filter dictionaryban /Length 16 szerepel, az Standard-handleres fájlban 128 bites kulcsot, public-key fájlban pedig elutasított fájlt jelent. A HotPDF ezt normalizálja a betöltött konfiguráció felvételekor. A /V2 filter /Length értékét csak akkor szorozza nyolccal, ha a fájl nem public-key titkosítású, saját filterérték hiányában visszaesik a dokumentumszintű /Length-re, az eredményt pedig a THPDFCryptFilterInfo.KeyLengthBits mezőben tárolja. Az AESV2 128 bitre, az AESV3 és AESV4 256 bitre van rögzítve, mert ezek a metódusok nem engednek kulcsméret-egyeztetést. A szigorú rész ezután jön: csak a 40 és 128 bites /V2 fogadható el. Ha egy filter más hosszra oldódik fel, elérhetetlenként jelenti és azonnal hibázik ahelyett, hogy 128-ra kerekítené azon az elméleten, hogy a legtöbb producer úgyis 128-at akart. A kulcshossz csendes normalizálásából lesz az a fájl, amely a saját gépeden dekódolódik, sehol máshol

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;
    // A /StrF és /StmF alapértéke Identity; az /EFF alapértéke /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;

Mit garantál a /CFM /None, és miben más az /Identity?

Más úton ugyanoda jutnak, az összekeverésük viszont tönkreteszi a feloldást. Egy olyan névvel ellátott filter, amelynek /CFM-je /None, illetve egy olyan, amelyből a /CFM teljesen hiányzik, egyaránt azt jelenti, hogy az adott filter nem végez titkosítást vagy visszafejtést — a HotPDF a hiányzó bejegyzést feloldás előtt None-ra képezi, így mindkettő hcfmNone lesz, rögzített nullás kulcshosszal. Az /Identity más természetű: ez a fenntartott név, amely teljesen megkerüli a /CF lookupot, ezért egy dokumentum úgy is hivatkozhat a /Identity-re, hogy sehol nem definiálja a /CF-ben. A PDF-nevek kis- és nagybetűérzékenyek, ezért egy további implementációs részlet nem alku tárgya: crypt filter lookup soha nem lehet case-insensitive. A HotPDF a /CF rész-dictionary neveit, a filter /Length bejegyzését és a stream /Type ellenőrzését is kis- és nagybetűérzékenyen oldja fel. A /stdcf-et definiáló, de az /StmF-ben /StdCF-re mutató fájl hibás, és ha ugyanannak tekintenéd a két kulcsot, egy felismerhető szerzői hibából csendben minden streamre rossz kulcsot alkalmazó fájlt csinálnál

Az /EFF érvényesítése a beágyazottfájl-streameken

Ha az /EFF különbözik az /StmF-től, a beágyazottfájl-streamnek explicit kezdő /Crypt bejegyzésre van szüksége a /Filter-ben, és ugyanazon tömbpozícióban egyező /Name-et hordozó /DecodeParms dictionaryra. A HotPDF ezt mentéskor, streamenként oldja meg: felismeri a /Type /EmbeddedFile-t, örökli a beállított beágyazottfájl-filtert, és csak akkor írja ki az explicit /Crypt jelölőt, ha az örökölt név eltér a stream tényleges alapértékétől. Ha az /EFF és az /StmF egyezik, nem ír jelölőt, mert a reader ugyanazt a filtert oldaná fel. A tömbpozíció legalább annyira számít, mint a név. Visszaolvasáskor a HotPDF megkeresi a /Crypt bejegyzést a /Filter-ben, rögzíti az indexét, majd a /DecodeParms tömbben ugyanazon az indexen keresi a /Name-et. Az 0. indexen lévő /Crypt az 1. indexen lévő paraméterekkel /Identity-re oldódik, nem a filteredre. Ezért tölti fel az író nullal a paramétertömböt, ha a streamnek korábban volt /Filter-je, de /DecodeParms-ja nem: a pozícióknak igazodniuk kell

Van egy élesebb csapda is. Ha a meglévő /Filter vagy /DecodeParms indirekt objektum — ez gyakori olyan generátorok fájljainál, amelyek egy filtertömböt sok stream között osztanak meg —, a /Crypt helyben beszúrása megváltoztatná a közös filtergráfot, és minden más, rá mutató streamet elrontana. A HotPDF feloldja az indirekt objektumot, majd előbb stream-privát direkt objektumba klónozza, az object és generation számokat nullázva, így az eredeti indirekt gyökér soha nem kerül az új tömbbe. Egy ASCIIHexDecode-ot már használó streamnél a szerializált eredmény /Filter [ /Crypt /ASCIIHexDecode ], a /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Ugyanez a pozíciós fegyelem irányít minden más filterláncot is, beleértve azokat, amelyeket a betöltött PDF képeinek dekódolási filtereken keresztüli kinyerésekor jársz be

// Az Editor már egy betöltött dokumentumot tart, a ContentStream pedig egy
// THPDFStreamObject, amelynek /Filter értéke indirekt /ASCIIHexDecode név
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');

// Az üres név törli a felülírást, és a következő mentéskor eltávolítja az
// elavult /Crypt bejegyzést a hozzá tartozó decode paraméterekkel együtt
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');

Öröklik az object streamek a dokumentum /Encrypt szabályát?

Nem, és ha ezt feltételezed, megbízhatóan szemetet állítasz elő. Egy object streamnek a tényleges /StmF szabályt vagy a saját explicit /Crypt jelölőjét kell követnie: az /Encrypt dictionary puszta jelenléte nem tesz minden /ObjStm konténert ciphertextté. Egy /StmF /Identity dokumentumban az object streamek plaintextként maradnak akkor is, ha a stringek teljesen titkosítottak, és a dekóder, amely mégis visszafejti őket, olyan bemenetet ad az inflate szakasznak, amely soha nem volt deflate kimenet

A member objektumokra vonatkozó következményt érdemes kétszer elolvasni. Az ISO 32000-1 §7.5.7 szerint egy titkosított object streamben lévő stringek már plaintextként állnak rendelkezésre, amikor magát a konténert visszafejtettük, ezért újratitkosításuk dupla-decrypt lenne. A HotPDF ezt úgy védi ki, hogy lekérdezi, titkosított volt-e az egyes type-2 objektum konténere, és ha igen, kihagyja az objektumot, a kihagyásokat pedig az XRefProbeDecryptObjStmSkips értékébe számolja, közvetlen bizonyítékként arra, hogy az őr működött. Ha a konténer plaintext volt, a benne lévő stringeket semmi nem fedte, ezért a HotPDF materializálja a memberöket, és külön-külön alkalmazza rájuk az /StrF-et — a tényleges implementáció szerint a member objektum- és generációszámával kulcsolva, nem a befoglaló /ObjStm objektumszámával. Vegyes szabályú fájlon ezt megfordítva minden tömörített objektumban zajjá dekódolódik az összes string. A konténerszintű szabályokat részletesebben a PDF object streamekről és inkrementális frissítésekről szóló jegyzet tárgyalja

Hol nem hajlandó találgatni a HotPDF?

A crypt filter szemantika /V 4 alatt nem létezik, ezért a HotPDF kifejezett hibával utasít el minden streamenkénti felülírást ilyen fájlon, ahelyett hogy olyan /Crypt jelölőt írna, amelyet egy szabványkövető reader sem venne figyelembe. Ugyanez igaz olvasáskor: a 4 alatti /V-t tartalmazó encryption dictionary mindhárom betöltött filternevet törli, mert nincs mit jelentenie. További három határt tudatosan kényszerít:

  • Public-key titkosítású dokumentum nem Identity értékű streamenkénti filterét elutasítja, mert a public-key handler alatti streamspecifikus szabályhoz olyan streamspecifikus recipient envelope kellene, amelyet a HotPDF még nem ír ki
  • A public-key titkosítású beágyazott fájlokat, amelyeknél az /EFF eltér a tényleges /StmF-től, ugyanezért utasítja el ahelyett, hogy olyan alakban írná ki, amelyet senki nem tud visszafejteni
  • Az AES-256 közvetlenfájl-felgyorsított útvonala csak akkor érvényes, ha a stringek, streamek és beágyazott fájlok ugyanarra a crypt filter metódusra oldódnak, és a fájl egyetlen objektuma sem hordoz explicit /Crypt-et; vegyes szabály vagy plaintext metaadat a teljes objektumgráfos útvonalra kényszerít

Ezek egyike sem teljesítménydöntés. Azt a helyet jelölik, ahol egy rossz találgatás olyan PDF-et eredményezne, amely egyik viewerben megnyílik, a másikban elbukik, és a fejlesztőnek semmilyen jelzést nem ad, amíg egy ügyfél nem jelenti. Egy ConfigureCryptFilterDefaults vagy mentés közbeni elutasítás egy kivételbe kerül; egy csendben rosszul kulcsolt beágyazott fájl egy teljes support-kört okoz. Ha Delphi vagy C++Builder szoftvert építesz, amely titkosított PDF-et állít elő vagy fogyaszt — szelektíven plaintext oldalcontentet titkosított mellékletekkel, PDF 2.0 titkosított payload-wrappereket, vagy olyan fájlokkal való interoperabilitást, amelyek crypt filter szabályait nem te választottad —, az itt leírt crypt filter API a jelenlegi HotPDF Delphi PDF component része, az általa használt encryption, object-stream és inkrementálisfrissítés-útvonalakkal együtt