HotPDF Delphi PDF -komponentti toteuttaa ISO 32000-1 §7.6.5:n crypt-filter-mallin kolmena erillisenä käytäntönä yhden kytkimen sijaan: ConfigureCryptFilterDefaults asettaa merkkijonosuodattimen /StrF, streamisuodattimen /StmF ja upotetun tiedoston suodattimen /EFF erikseen, SetStreamCryptFilter ohittaa yhden streamin asetuksen ja GetLoadedCryptFilterInfo raportoi mitä tuleva tiedosto ilmoittaa. Useimmat salattujen PDF-tiedostojen yhteentoimivuusvirheet elävät näiden kolmen välisissä aukoissa
Tässä on vika joka ajaa ihmiset tämän kerroksen pariin. Tiimi toimittaa dokumentin, jossa sivusisällön pitää pysyä downstream-työkalun luettavana mutta liitetty sisältö ei saa olla, joten he asettavat /EFF /StdCF ja jättävät arvon /StmF /Identity. Acrobat avaa tiedoston ongelmitta. Vaatimustenmukainen kolmannen osapuolen lukija palauttaa liitteen salakirjoitettuna roskana, koska /EFF on tuottajapuolen käytäntö siitä mitä suodatinta upotettuihin tiedostoihin sovelletaan ja yleinen lukija ratkaisee merkitsemättömän streamin edelleen /StmF-suodattimen kautta. Korjaus ei ole toinen /EFF-arvo. Korjaus on eksplisiittinen /Crypt-suodatin itse upotetun tiedoston streamiin
Mitä crypt-filter-kerros oikeasti ohjaa
Crypt-filterit sijaitsevat salausalgoritmin ja objektigraafin välissä, ja ne päättävät mihin objekteihin algoritmi vaikuttaa, eivät miten se toimii. Salausdictionaryn sisällä oleva /CF-dictionary yhdistää nimet suodatinmäärittelyihin, joista kukin sisältää /CFM-metodin, valinnaisen /Length-arvon ja /AuthEvent-arvon. Kolme ylätason merkintää /StrF, /StmF ja /EFF valitsevat sitten, mitä nimettyä suodatinta käytetään merkkijonoihin, ilman eksplisiittistä suodatinta oleviin streameihin ja upotettuihin tiedostoihin. HotPDF rajoittaa tarkoituksella sitä mitä sen sisäänrakennetut käsittelijät kirjoittavat. ConfigureCryptFilterDefaults hyväksyy vain aktiivisen käsittelijän varatut nimet: Standard security handler tuottaa nimet /StdCF tai /Identity, public-key-käsittelijä tuottaa nimet /DefaultCryptFilter tai /Identity, ja kaikki muu nostaa EArgumentException-poikkeuksen kutsukohdassa. Ulkoisten tuottajien muilla nimillä kirjoittamat suodattimet säilytetään silti lataus-, tarkastus- ja yhteensopivuusuudelleenkirjoituksen poluilla, joten HotPDF on kirjoittajana konservatiivinen ja lukijana salliva. Lisäksi on kaksi suojaa: kutsu nostaa EInvalidOpException-poikkeuksen kun dokumentin serialisointi on alkanut ja uudelleen, jos dokumentti on inkrementaalisessa päivityksessä, koska salausta koskevaa käytäntöä ei voi vaihtaa saman tiedoston revisioiden välillä
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;
// merkkijonot salataan, sivustreamit ovat selväkielisiä, liitteet salataan
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;
Yksi rajoitus kannattaa sanoa heti aluksi, koska se tarkistetaan myöhään ja yllättää ihmiset. HotPDF:n nimetyt crypt-filterit vaativat dokumentin salaukseksi aes128-, aes256- tai aesgcm-arvon. Määritä suodatinpolitiikka RC4:n k40- tai k128-arvon päälle, niin salauksen käyttöönoton yhteydessä ajettava validointivaihe nostaa virheen sen sijaan että muuttaisi avaintyypin hiljaa ylemmäksi. Tämä on sama suunnittelukanta kuin Delphin AES-256 PDF -salauspolulla: epäselvä kokoonpano hylätään sen sijaan että arvailtaisiin mitä kutsuja tarkoitti
Miksi /Length-merkintä tarkoittaa kahta eri asiaa?
Koska spesifikaatio määrittelee sen kahdessa eri yksikössä suojauskäsittelijästä riippuen ja HotPDF:n täytyy kunnioittaa molempia. Crypt-filter-dictionaryssä, jonka /CFM on /V2, /Length-merkintä ilmaistaan Standard security handlerin alla tavuina ja public-key-käsittelijän alla bitteinä. Salausdictionaryn /Length, joka sijaitsee /V-arvon rinnalla (ISO 32000-1 §7.6.2), on aina bitteinä. Lue filter-dictionary, jossa on /Length 16, ja sinulla on Standard-handlerin tiedostossa 128-bittinen avain mutta public-key-tiedostossa hylättävä tiedosto. HotPDF normalisoi tämän tallentaessaan ladatun asetuksen. Se kertoo /V2-suodattimen /Length-arvon kahdeksalla vain, kun tiedosto ei ole public-key-salattu, käyttää dokumenttitason /Length-arvoa jos suodatin jättää omansa pois ja tallentaa tuloksen kenttään THPDFCryptFilterInfo.KeyLengthBits. AESV2 lukitaan 128 bittiin ja AESV3 sekä AESV4 256 bittiin, koska näillä metodeilla ei ole neuvoteltavaa avainkokoa. Tiukka osa tulee seuraavaksi: vain 40-bittinen ja 128-bittinen /V2 hyväksytään. Suodatin joka ratkaisee jonkin muun pituuden ilmoitetaan käytettävissä olemattomaksi ja operaatio epäonnistuu sen sijaan että se pyöristettäisiin 128 bittiin ajatuksella jonka mukaan useimmat tuottajat tarkoittivat kuitenkin 128:aa. Avainpituuden hiljainen normalisointi tuottaa tiedoston joka purkautuu omalla koneellasi mutta ei missään muualla
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 ja /StmF ovat oletuksena Identity; /EFF käyttää oletuksena /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ä /CFM /None takaa ja miten /Identity eroaa?
Ne päätyvät samaan lopputulokseen eri reittejä ja niiden sekoittaminen rikkoo haut. Nimetty suodatin jonka /CFM on /None sekä nimetty suodatin joka jättää /CFM-arvon kokonaan pois tarkoittavat molemmat ettei tämä suodatin salaa tai pura salausta lainkaan — HotPDF yhdistää puuttuvan merkinnän arvoon None ennen ratkaisua, joten molemmat päätyvät arvoon hcfmNone ja tallennettuun nolla-avainpituuteen. /Identity on luonteeltaan erilainen: se on varattu nimi joka ohittaa /CF-haun kokonaan, joten dokumentti voi viitata nimeen /Identity määrittelemättä sitä missään /CF-dictionarystä. PDF-nimet erottavat isot ja pienet kirjaimet, mikä tekee yhdestä toteutusyksityiskohdasta ehdottoman: mikään crypt-filter-haku ei saa olla kirjainkoon suhteen riippumaton. HotPDF ratkaisee /CF-altdictionaryn nimet, suodattimen /Length-merkinnän ja streamin /Type-tarkistuksen kirjainkokoherkillä dictionary-hauilla. Tiedosto joka määrittelee arvon /stdcf samalla kun /StmF viittaa arvoon /StdCF on virheellinen, ja niiden käsitteleminen samana avaimena muuttaisi havaittavan tekijän virheen hiljaisesti jokaiseen dokumentin streamiin sovellettavaksi vääräksi avaimeksi
/EFF:n kiinnittäminen upotettujen tiedostojen streameihin
Kun /EFF eroaa arvosta /StmF, upotetun tiedoston stream tarvitsee eksplisiittisen alussa olevan /Crypt-merkinnän /Filter-taulukkoonsa ja sitä vastaavan /DecodeParms-dictionaryn, jossa on /Name-arvo samassa taulukkopaikassa. HotPDF ratkaisee tämän streamikohtaisesti tallennushetkellä: se tunnistaa /Type /EmbeddedFile-tyypin, perii asetetun upotetun tiedoston suodattimen ja kirjoittaa eksplisiittisen /Crypt-merkin vain silloin kun peritty nimi eroaa streamin tehokkaasta oletussuodattimesta. Kun /EFF ja /StmF täsmäävät, merkkiä ei kirjoiteta, koska lukija ratkaisisi joka tapauksessa saman suodattimen. Taulukon indeksillä on silloin yhtä paljon merkitystä kuin nimellä. Kun HotPDF lukee streamin takaisin, se etsii /Filter-taulukosta /Crypt-merkinnän, tallentaa sen indeksin ja etsii sitten saman indeksin /DecodeParms-taulukosta löytääkseen /Name-arvon. Indeksissä 0 oleva /Crypt, jonka parametrit ovat indeksissä 1, ratkeaa arvoon /Identity eikä omaan suodattimeesi. Siksi kirjoittaja täyttää parametrientaulukon null-arvolla, kun streamilla oli aiemmin /Filter mutta ei /DecodeParms-taulukkoa: paikkojen täytyy pysyä kohdistettuina
Alla on vielä terävämpi ansa. Jos olemassa oleva /Filter tai /DecodeParms on epäsuora objekti — tavallista tuottajilla jotka jakavat yhden filter-taulukon monen streamin kesken — /Crypt-merkin lisääminen suoraan muuttaisi jaettua suodatinkaaviota ja korruptoisi jokaisen siihen osoittavan muun streamin. HotPDF ratkaisee epäsuoran objektin ja kloonaa sen ensin streamin omaksi suoraksi objektiksi, tyhjentää objektin ja generoinnin numerot, eikä alkuperäistä epäsuoraa juurta koskaan upoteta uuteen taulukkoon. Streamille joka käytti jo ASCIIHexDecode-suodatinta serialisoitu tulos on /Filter [ /Crypt /ASCIIHexDecode ] ja /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Sama indeksikuri ohjaa jokaista muuta suodatinketjua, myös niitä joita kävelet kun poimit kuvia ladatusta PDF:stä niiden dekoodausfilttien kautta
// Editor pitää jo ladattua dokumenttia, ja ContentStream on
// THPDFStreamObject, jonka /Filter on epäsuora /ASCIIHexDecode-nimi
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');
// Tyhjä nimi poistaa ohituksen ja riisuu vanhentuneen /Crypt-merkin
// sekä sen decode-parametrit seuraavan tallennuksen yhteydessä
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Perivätkö object streamit dokumentin /Encrypt-käytännön?
Eivät, ja sen olettaminen on varma tapa tuottaa roskaa. Objektistreamin täytyy seurata todellista /StmF-käytäntöä tai omaa eksplisiittistä /Crypt-merkintäänsä: pelkkä /Encrypt-dictionaryn olemassaolo ei tee jokaisesta /ObjStm-säiliöstä salakirjoitettua. Dokumentissa jonka /StmF /Identity objektistreamit ovat selväkielisiä, vaikka sen merkkijonot olisi salattu kokonaan, ja dekooderi joka purkaa ne silti syöttää inflate-vaiheeseen dataa jota ei ole koskaan deflatoitu
Jäsenobjektien seuraus on kohta joka kannattaa lukea kahdesti. ISO 32000-1 §7.5.7:n mukaan salatussa objektistreamissa olevat merkkijonot ovat jo selväkielisiä, kun säiliö itse on purettu, joten niiden purkaminen uudelleen olisi kaksoispurku. HotPDF estää tämän kysymällä oliko kunkin type-2-objektin säiliö salattu ja ohittamalla objektin silloin kun se oli, ja laskee ohitukset kenttään XRefProbeDecryptObjStmSkips suorana näyttönä siitä että suoja laukesi. Kun säiliö oli selväkielinen, jäsenmerkkijonoja ei suojannut mikään, joten HotPDF materialisoi jäsenet ja käyttää /StrF-suodatinta kuhunkin erikseen — avaimena, kuten toteutus oikeasti tekee, jäsenobjektin numeroa ja generointia eikä sisältävää /ObjStm-objektinumeroa. Käännä tämä päinvastoin sekakäytäntöisessä tiedostossa ja jokainen merkkijono jokaisessa pakatussa objektissa dekoodautuu kohinaksi. Tämän ympärillä olevia säiliötason sääntöjä käsitellään lisää muistiinpanoissa PDF-objektistreameista ja inkrementaalisista päivityksistä
Missä HotPDF kieltäytyy arvaamasta
Crypt-filter-semantiiikkaa ei ole olemassa arvon /V 4 alapuolella, joten HotPDF hylkää streamikohtaisen ohituksen tällaisessa tiedostossa eksplisiittisellä virheellä sen sijaan että kirjoittaisi /Crypt-merkin jota mikään vaatimustenmukainen lukija ei kunnioittaisi. Sama pätee lukupuolella: salausdictionary jonka /V on alle 4 tyhjentää kaikki kolme ladattua suodatinimeä, koska raportoitavaa ei ole. Lisäksi valvotaan tarkoituksella kolmea rajaa:
- Julkisen avaimen salatussa dokumentissa kieltäydytään muusta kuin
Identity-arvosta streamikohtaisena suodattimena, koska public-key-käsittelijän streamikohtainen käytäntö vaatisi streamikohtaisen vastaanottajan säiliön jota HotPDF ei vielä tuota - Julkisen avaimen salatut upotetut tiedostot joiden
/EFFeroaa tehokkaasta/StmF-arvosta hylätään samasta syystä sen sijaan että ne kirjoitettaisiin muotoon joka ei purkaudu kenellekään - AES-256:n suora tiedostojen nopea polku toimii vain silloin kun merkkijonot, streamit ja upotetut tiedostot ratkeavat samalle crypt-filter-metodille eikä missään tiedoston objektissa ole eksplisiittistä
/Crypt-merkintää; sekakäytäntö tai selväkieliset metatiedot pakottavat vararatkaisun koko objektigraafin polulle
Mikään näistä ei ole suorituskykypäätös. Ne merkitsevät kohtia, joissa väärä arvaus tuottaa PDF:n joka avautuu yhdessä katseluohjelmassa, epäonnistuu toisessa ja ei anna kehittäjälle mitään signaalia ennen kuin asiakas ilmoittaa ongelmasta. Kieltäytyminen kohdassa ConfigureCryptFilterDefaults tai tallennushetkellä maksaa yhden poikkeuksen; hiljaisesti väärin avattu upotettu tiedosto maksaa tukikierroksen. Jos rakennat Delphillä tai C++Builderilla ohjelmistoa joka tuottaa tai kuluttaa salattua PDF:ää — valikoivasti selväkielistä sivusisältöä salatuilla liitteillä, PDF 2.0:n salattuja payload-wrapper-rakenteita tai yhteentoimivuutta tiedostojen kanssa joiden crypt-filter-käytäntöä et valinnut — tässä kuvattu crypt-filter-API toimitetaan nykyisessä HotPDF Delphi PDF -komponentissa yhdessä sen varaan rakennettujen salaus-, objektistream- ja inkrementaalisten päivityspolkujen kanssa