Pred različico 3.114.8 je PDFiumPas material ključev za šifriranje PDF na ciljih izven Windows ustvarjal s funkcijo Random iz izvajalne knjižnice, in ker nič ni poklicalo Randomize, je vsak proces proizvedel isto zaporedje bajtov. Gradnje Free Pascal na Linuxu in macOS so torej zapisovale identične ključe za šifriranje datotek, soli, IV CBC in predpone nonce AES-GCM, zagon za zagonom. Različica 3.114.8 namesto tega bere /dev/urandom in sproži izjemo, kadar tega ne more
Napaka sama je zanka s štirimi vrsticami. Koristnejša lekcija je, zakaj je testna zbirka, ki šifrira in dešifrira stotine dokumentov, z AESV3 in AESV4, s PDF MAC in brez, ves čas ostala zelena. Naključnost, konstantna na proces, je nevidna vsakemu preizkusu, ki teče znotraj enega procesa — in točno tako se po navadi pišejo testi šifriranja
Kje PDFiumPas potrebuje naključne bajte?
Vsak naključni bajt v skladu za šifriranje PDFiumPas prihaja iz enega samega postopka, AesGenerateRandomBytes v enoti FPdfAes, zato en slab vir onesnaži vse skupaj. Standardni varnostni upravljalnik v ISO 32000-2 §7.6.4 in razširitev AESV4 v ISO/TS 32003 porabijo te bajte na teh mestih:
- 32-bajtni ključ za šifriranje datoteke, ki ga DeriveEncryptionKeys za vsak dokument ustvari na sveže in ga potem ovije v /UE in /OE pod ključi, izpelanimi iz gesla
- Dve 16-bajtni soli, ena shranjena v zadnjih 16 bajtih /U in ena v zadnjih 16 bajtih /O, vsaka razdeljena na 8-bajtno validacijsko sol in 8-bajtno ključno sol
- Bajti 12 do 15 čistega besedila za /Perms, ki jih ISO 32000-2 zapolni z naključnimi podatki, preden je blok šifriran pod ključem datoteke
- 16-bajtni IV CBC, postavljen pred vsak šifriran niz in tok v dokumentu AESV3
- 8-bajtna predpona nonce za dokumente AESV4, ki ji sledi 4-bajtni števec na objekt, ki se začne z nič
- 32-bajtni /KDFSalt in ključ MAC, kadar je nastavljen EnableIntegrityProtection
Zakaj je vsak proces proizvedel isti ključ?
AesGenerateRandomBytes je generator operacijskega sistema uporabljal le na Windows; povsod drugje je zapolnil medpomnilnik iz psevdonaključnega generatorja RTL, ta pa se začne z RandSeed = 0, če program ne pokliče Randomize. Komentar nad zanko je trdil, da je bil generator oskrbljen s semenom iz GetTickCount64. Nobena vrstica kode tega nikoli ni naredila, zato je bil komentar edino mesto, kjer je seme sploh obstajalo:
// Ne-Windows veja AesGenerateRandomBytes pred 3.114.8
// (komentar nad njo je obljubil seme GetTickCount64, ki ga ni nikoli dobila)
P := PByte(Buffer);
for I := 0 to Count - 1 do
P[I] := Byte(Random(256));
Zaporedje se ponovno začne pri vsakem procesu in v njem napreduje, zato prvi dokument, ki ga kateri koli proces šifrira, deli svoj ključ datoteke s prvim dokumentom vsakega drugega procesa, ki poganja isto gradnjo, drugi z drugim in tako naprej. Ključ datoteke v R5, R6 in R7 sploh ni odvisen od gesla, saj ga geslo le ovije, kar pomeni, da kdor zmore ponoviti zaporedje, drži ključ v rokah brez poznavanja gesla. AESV4 doda drugo odpoved: isti ključ z isto 8-bajtno predpono in števecem, ki se ponovno začne z nič, ponavlja nece GCM, kar NIST SP 800-38D §8 prepoveduje brez oklevanja. Ponovljen nonce GCM pod istim ključem razkrije XOR obeh čistih besedil in izpostavi podključ za overjanje, zato oznake, na katere se zanaša šifriranje AESV4-GCM in žeton PDF MAC, nehajo pomeniti karkoli. Zaupnost in celovitost odideta hkrati
Obseg je ožji, kot bi lahko nakazal ta odstavek. Gradnje Windows nikoli niso bile prizadete, ker veja Windows vedno pokliče CryptGenRandom prek advapi32 s CRYPT_VERIFYCONTEXT in sproži izjemo, kadar to spodleti. Izpostavljen je bil izhod ne-Windows gradnji, starejših od 3.114.8, kar v praksi pomeni aplikacije Lazarus in Free Pascal na Linuxu in macOS — še en vnos na seznam pasti Delphi proti FPC v gradnjah PDFium
Zakaj Randomize nikoli ni bila prava rešitev?
Klic Randomize bi skril simptom, ne da bi popravil vir, ker je RandSeed 32-bitna vrednost, Randomize pa jo izpelje iz ure. To število možnih ključnih tokov omeji na 2^32, poznavanje približnega trenutka nastanka datoteke pa iskanje zreže daleč pod to — ob 256-bitnem ključu AES to ni nič. Ključni material mora prihajati iz bazena entropije jedra, zato AesGenerateRandomBytes v 3.114.8 bere /dev/urandom, obrača zanko čez kratke bralne operacije in sproži izjemo, kadar bazen ne more dostaviti vsakega zahtevanega bajta:
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; // napaka ali nepričakovan konec toka
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');
Zavrnitev je namerna in se ujema s tem, kar je veja Windows vedno naredila, kadar CryptGenRandom ni na voljo. Spodletelo šifrirano shranjevanje je primer, ki ga opazite isti dan; uspešno shranjevanje s predvidljivimi ključi pa je tisto, o katerem izveste od nekoga drugega. Sledita dve praktični posledici. Minimalen vsebnik ali chroot brez napolnjenega /dev zdaj spodleti pri šifriranju, namesto da bi tiho degradiral, zato ga priklopite. In ker se izjema razširi ven iz TPdf.SaveAsEncrypted potem, ko je bila ciljna datoteka odprta s fmCreate, za sabo pusti prazno izhodno datoteko, ki jo naj Vaš error handler izbriše
Zakaj round-trip testi tega nikoli niso ujeli?
Round-trip preizkus konstantne naključnosti ne vidi, ker dešifriranje povrne kateri koli ključ datoteke, ki ga je šifriranje izbralo. Preizkus dokument zašifrira, ga znova odpre z geslom, razvije ključ iz /UE in dešifrira vsak objekt; predvidljiv ključ se razvije in dešifrira prav tako dobro kot naključen, oznake GCM pa se potrdijo, ker so bile izračunane z istim ključem. Celo preizkus, ki šifrira dvakrat in zagotovi, da se izhoda razlikujeta, gre skozi, ker drugi klic v istem procesu potegne naslednje bajte zaporedja. Lastnost, ki šteje — drug ključ v vsakem procesu — je opazovljiva le s primerjavo izhodov čez procese. Kadar koli ista koda tako proizvaja kot porablja vrednost, so testi slepi za cele razrede napak, naključnost pa je najčistejši primer
Kako preizkusite naključnost ključev čez procese?
Poženite majhno sondo dvakrat kot ločena procesa na ciljni platformi in primerjajte izhod. Sonda spodaj pokliče DeriveEncryptionKeys in izpiše sol, shranjeno v bajtih 32 do 47 vnosa /U. Ta vrednost je vidno zapisana v vsako šifrirano datoteko, izpis v dnevnikih CI torej ne razkrije ničesar, prihaja pa iz istega generatorja kot ključ datoteke:
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-bajtni hash + 16-bajtna sol
Hex := Hex + IntToHex(Keys.UEntry[I], 2);
WriteLn(Hex); // se mora razlikovati pri vsakem zagonu
end.
Priključite sondo v gradnjo za vsak cilj izven Windows: poženite jo dvakrat in spodletite opravilo, če se vrstici ujemata. Ista primerjava deluje na datotekah, ki so že v obtoku. Vzemite dva šifrirana PDF, napisana ob različnih zagonih iste aplikacije, preberite niza /U iz njunih slovarjev Encrypt in primerjajte zadnjih 16 bajtov; identični soli prepoznavata prizadeto gradnjo, dokumenta pa ju je treba znova zašifrirati iz čistega besedila s 3.114.8 ali novejšo, tako da vsak dobi svež ključ datoteke. Splošna navada naj bo, da ne-Windows poti kode vadite na sami platformi, namesto da zaupate zagonu Windows — isto razmišljanje stoji za časovnim zaledjem libcurl za gradnje izven Windows
PDFiumPas je komponenta PDF za Delphi in Lazarus, zgrajena na pogonu PDFium, z AES-256, AES-GCM in žetonom PDF MAC, implementiranim izvorno v Pascalu, ključni material pa se vleče iz generatorja operacijskega sistema na vsaki platformi. Podrobnosti in prenosi so na strani komponente PDFium za Delphi