Ennen versiota 3.114.8 PDFiumPas generoi PDF-salauksen avaimateriaalia ei-Windows-kohdeympäristöissä ajonaikaisen kirjaston Random-funktiolla, ja koska mikään ei kutsunut Randomizea, jokainen prosessi tuotti saman tavusarjan. Linuxin ja macOS:n Free Pascal -käännökset kirjoittivat siksi identtiset tiedostonsalausavaimet, suolat, CBC-IV:t ja AES-GCM-nonce-etuliitteet ajo ajoalta. Versio 3.114.8 lukee /dev/urandomin ja nostaa poikkeuksen kun se ei onnistu
Vika itsessään on nelirivinen silmukka. Hyödyllisempi opetus on se miksi testipaketti joka salaa ja purkaa satoja asiakirjoja, AESV3:lla ja AESV4:llä, PDF MAC:n kanssa ja ilman, pysyi vihreänä koko ajan. Prosessia kohti vakio satunnaisuus on näkymätön jokaiselle yhdessä prosessissa ajetalle testille, ja juuri niin salaus-testit yleensä kirjoitetaan
Missä PDFiumPas tarvitsee satunnaisia tavuja?
Jokainen satunnainen tavu PDFiumPas:n salauspinossa tulee yhdestä proseduurista, AesGenerateRandomBytes FPdfAes-yksikössä, joten yksi huono lähde saastuttaa kaiken. ISO 32000-2:n §7.6.4:n standarditurvauskäsittelijä ja ISO/TS 32003:n AESV4-laajennus kuluttavat kyseiset tavut näissä paikoissa:
- 32-tavuinen tiedostonsalausavain, jonka DeriveEncryptionKeys generoi tuoreena jokaiselle asiakirjalle ja joka sitten kääritään /UE:hen ja /OE:hen salasanasta johdettujen avainten alle
- Kaksi 16-tavuista suolaa, toinen tallennettuna /U:n viimeisiin 16 tavuun ja toinen /O:n viimeisiin 16 tavuun, kumpikin jaettuna 8-tavuiseen validointisuolaan ja 8-tavuiseen avainsuolaan
- /Permsin takana olevan selkotekstin tavut 12–15, jotka ISO 32000-2 täyttää satunnaisella datalla ennen kuin lohko salataan tiedostoavaimen alla
- 16-tavuinen CBC-IV joka liitetään eteen jokaiseen salattuun merkkijonoon ja streamiin AESV3-asiakirjassa
- 8-tavuinen nonce-etuliite AESV4-asiakirjoille, jota seuraa 4-tavuinen objektikohtainen laskuri joka alkaa nollasta
- 32-tavuinen /KDFSalt ja MAC-avain kun EnableIntegrityProtection on asetettu
Miksi jokainen prosessi tuotti saman avaimen?
AesGenerateRandomBytes käytti käyttöjärjestelmän generaattoria vain Windowsissa; kaikkialla muualla se täytti puskurin RTL:n pseudo-satunnaisgeneraattorista, ja se generaattori alkaa arvosta RandSeed = 0 ellei ohjelma kutsu Randomizea. Silmukan yläpuolella oleva kommenti sanoi generaattorin kylvetyn GetTickCount64:stä. Mikään koodirivi ei koskaan tehnyt sitä, mistä tuli se että kommenti oli ainoa paikka missä siemen oli olemassa:
// AesGenerateRandomBytesin ei-Windows-haara ennen 3.114.8
// (sen yläpuolella oleva kommenti lupasi GetTickCount64-siemenen jota ei koskaan sovellettu)
P := PByte(Buffer);
for I := 0 to Count - 1 do
P[I] := Byte(Random(256));
Sarja alkaa alusta jokaisessa prosessissa ja etenee sen sisällä, joten ensimmäinen asiakirja jonka mikä tahansa prosessi salaa jakaa tiedostoavaimensa jokaisen saman käännöksen ajavan muun prosessin ensimmäisen asiakirjan kanssa, toinen toisen kanssa ja niin edelleen. Tiedostoavain R5:ssä, R6:ssa ja R7:ssä ei riipu salasanasta lainkaan, koska salasana vain käärii sen, mikä tarkoittaa että kuka tahansa joka pystyy toistamaan sarjan pitää avainta tietämättä salasanaa. AESV4 lisää toisen vian: sama avain samalla 8-tavuisella etuliitteellä ja nollasta käynnistyvällä laskurilla toistaa GCM-nonceja, mikä on NIST SP 800-38D §8:n ehdottomasti kieltämä. Toistettu GCM-nonce yhden avaimen alla paljastaa kahden selkotekstin XORin ja paljastaa autentikoinnin aliavaimen, joten AESV4-GCM-salauksen ja PDF MAC -tokenin nojaamat tagit lakkaavat merkitsemästä mitään. Salassapito ja eheys lähtevät yhtä aikaa
Laajuus on kapeampi kuin se kappale voi antaa ymmärtää. Windows-käännökset eivät koskaan kärsineet, koska Windows-haara on aina kutsunut CryptGenRandomia advapi32:n kautta CRYPT_VERIFYCONTEXTilla ja nostanut poikkeuksen jos se epäonnistui. Paljastunutta oli ei-Windows-käännösten tuotos ennen versiota 3.114.8, mikä käytännössä tarkoittaa Lazarus- ja Free Pascal -sovelluksia Linuxissa ja macOS:ssä, yksi merkintä lisää Delphin ja FPC:n sudenkuoppien listaan PDFium-käännöksissä
Miksi Randomize ei ollut koskaan oikea korjaus?
Randomizen kutsuminen olisi peittänyt oireen korjaamatta lähdettä, koska RandSeed on 32-bittinen arvo ja Randomize johtaa sen kellosta. Se kattoo mahdollisten avainvirtojen määrän arvoon 2^32, ja karkea tieto siitä milloin tiedosto kirjoitettiin leikkaa haun paljon kauemmas alle sen, mikä ei ole mitään 256-bittisen AES-avaimen rinnalla. Avaimateriaalin on tultava kernelin entropiapoolista, joten AesGenerateRandomBytes 3.114.8:ssa lukee /dev/urandomia, silmukoi lyhyiden lukujen yli ja nostaa poikkeuksen jos pooli ei pysty toimittamaan jokaista pyydettyä tavua:
Handle := FileOpen('/dev/urandom', fmOpenRead or fmShareDenyNone);
if Handle <> THandle(-1) then
try
Remaining := Count;
while Remaining > 0 do
begin
Got := FileRead(Handle, P^, Remaining);
if Got <= 0 then
Break; // vika tai odottamaton streamin loppu
Inc(P, Got);
Dec(Remaining, Got);
end;
if Remaining = 0 then
Exit;
finally
FileClose(Handle);
end;
raise Exception.Create('FPdfAes: /dev/urandom unavailable; refusing to emit predictable key/IV');
Kieltäytyminen on tahallista, ja se vastaa sitä mitä Windows-haara on aina tehnyt kun CryptGenRandom ei ole saatavilla. Epäonnistunut salattu tallennus on tapaus jonka huomaat samana päivänä; onnistunut tallennus ennustettavilla avaimilla on sellainen josta kuulet joltakulta muulta. Kaksi käytännön seurausta seuraa. Minimaalinen kontti tai chroot jolla ei ole täytettyä /dev:tä epäonnistuu nyt salauksessa hiljaisen heikkenemisen sijaan, joten mounttaa se. Ja koska poikkeus leviää ulos TPdf.SaveAsEncryptedista sen jälkeen kun kohdetiedosto avattiin fmCreatella, tyhjä tulostiedosto jää jäljelle virheenkäsittelijäsi poistettavaksi
Miksi round trip -testit eivät koskaan löytäneet sitä?
Round trip -testi ei näe vakiota satunnaisuutta, koska purku palauttaa sen tiedostoavaimen minkä salaus valitsi. Testi salaa asiakirjan, avaa sen uudelleen salasanalla, käärii avaimen auki /UE:stä ja purkaa jokaisen objektin; ennustettava avain kääriytyy ja purkautuu yhtä hyvin kuin satunnainenkin, ja GCM-tagit verifioituvat koska ne laskettiin samalla avaimella. Jopa testi joka salaa kahdesti ja vaatii kahden tuloksen eroavan läpäisee, koska toinen kutsu samassa prosessissa nostaa sarjan seuraavat tavut. Ominaisuus jolla on väliä, eri avain joka prosessissa, on havaittavissa vain vertaamalla tuotosta prosessien välillä. Aina kun sama koodi sekä tuottaa että kuluttaa arvon, testit ovat sokeita kokonaisille vikaluokille, ja satunnaisuus on puhtain esimerkki
Miten voit testata avainten satunnaisuutta prosessien välillä?
Aja pieni anturi kahdesti erillisinä prosesseina kohdealustalla ja vertaa tuotosta. Alla oleva anturi kutsuu DeriveEncryptionKeysia ja tulostaa /U-merkinnän tavuihin 32–47 tallennetun suolan. Se arvo kirjoitetaan näkyvästi jokaiseen salattuun tiedostoon, joten sen tulostaminen CI-lokeihin ei paljasta mitään, ja silti se tulee samasta generaattorista kuin tiedostoavain:
program SaltProbe;
{$mode delphi}
uses
SysUtils, FPdfEncrypt;
var
Opts: TPdfEncryptOptions;
Keys: TPdfEncryptionKeys;
I: Integer;
Hex: string;
begin
Opts := TPdfEncryptOptions.Default;
Opts.UserPassword := 'probe';
Opts.Revision := erR6;
DeriveEncryptionKeys(Opts, Keys);
Hex := '';
for I := 32 to 47 do // /U = 32-tavuinen tiiviste + 16-tavuinen suola
Hex := Hex + IntToHex(Keys.UEntry[I], 2);
WriteLn(Hex); // täytyy erota jokaisella ajolla
end.
Kytkä anturi käännökseen jokaiselle ei-Windows-kohteelle: aja se kahdesti ja hylkää työ jos rivit täsmäävät. Sama vertailu toimii jo kiertoon laskettuihin tiedostoihin. Ota kaksi salattua PDFää jotka eri ajot samasta sovelluksesta kirjoittivat, lue /U-merkkijonot niiden Encrypt-sanakirjoista ja vertaa viimeisiä 16 tavua; identtiset suolat tunnistavat vaikutuksen alaisen käännöksen, ja asiakirjat kannattaa salata uudelleen niiden selkotekstistä versiolla 3.114.8 tai uudemmalla jotta jokainen saa tuoreen tiedostoavaimen. Yleinen tapa on harjoitella ei-Windows-koodipolkuja itse alustalla sen sijaan että luottaisi Windows-ajoon, sama päättely jonka takana on libcurl-aikaleintabackend ei-Windows-käännöksille
PDFiumPas on Delphi- ja Lazarus-PDF-komponentti joka rakentuu PDFium-moottorin päälle, AES-256, AES-GCM ja PDF MAC -token toteutettuina natiivisti Pascalissa ja avaimateriaali haettu käyttöjärjestelmän generaattorista joka alustalla. Yksityiskohdat ja lataukset ovat PDFium Delphi -komponentin sivulla