Pre verzije 3.114.8, PDFiumPas je generisao materijal PDF ključeva šifrovanja na ne-Windows targetima runtime bibliotečkom funkcijom Random, i pošto ništa nije zvalo Randomize, svaki je proces proizvodio istu sekvencu bajtova. Free Pascal buildovi na Linux-u i macOS-u su zato pisali identične ključeve šifrovanja fajla, soli, CBC IV-ove i AES-GCM nonce prefikse pokretanje za pokretanjem. Verzija 3.114.8 umesto toga čita /dev/urandom i podiže izuzetak kad ne može
Sam defekt je petlja od četiri linije. Korisnija lekcija je zašto je test suite koji šifruje i dešifruje stotine dokumenata, sa AESV3 i AESV4, sa PDF MAC-om i bez njega, bio zelen ceo tog vremena. Slučajnost koja je konstantna po procesu nevidljiva je svakom testu koji radi unutar jednog procesa, i baš tako se obično pišu testovi šifrovanja
Gde PDFiumPas treba slučajne bajtove?
Svaki slučajan bajt u PDFiumPas encryption stack-u dolazi iz jedne procedure, AesGenerateRandomBytes u jedinici FPdfAes, pa jedan loš izvor kontaminira sve. Standardni security handler u ISO 32000-2 §7.6.4 i AESV4 ekstenzija u ISO/TS 32003 troše te bajtove na ovim mestima:
- 32-bajtni ključ šifrovanja fajla, svež generisan od DeriveEncryptionKeys za svaki dokument pa umotan u /UE i /OE pod ključevima izvedenim iz lozinke
- Dve soli od 16 bajtova, jedna smeštena u poslednjih 16 bajtova /U i jedna u poslednjih 16 bajtova /O, svaka podeljena na 8-bajtnu validation so i 8-bajtnu key so
- Bajtovi 12 do 15 otvorenog teksta iza /Perms, koje ISO 32000-2 puni slučajnim podacima pre nego što se blok šifruje ključem fajla
- 16-bajtni CBC IV koji se dodaje ispred svakog šifrovanog string-a i stream-a u AESV3 dokumentu
- 8-bajtni nonce prefiks za AESV4 dokumente, praćen 4-bajtnim brojačem po objektu koji kreće od nule
- 32-bajtni /KDFSalt i MAC ključ kad je EnableIntegrityProtection postavljen
Zašto je svaki proces proizvodio isti ključ?
AesGenerateRandomBytes je koristio generator operativnog sistema samo na Windows-u; svuda je drugde punio bafer iz RTL pseudo-slučajnog generatora, i taj generator kreće od RandSeed = 0 osim ako program pozove Randomize. Komentar iznad petlje tvrdio je da je generator semenjen iz GetTickCount64. Nijedna linija koda to nikada nije učinila, pa je komentar bio jedino mesto gde je seme postojalo:
// Ne-Windows grana AesGenerateRandomBytes pre 3.114.8
// (komentar iznad nje obećavao je GetTickCount64 seme koje nikada nije primenjeno)
P := PByte(Buffer);
for I := 0 to Count - 1 do
P[I] := Byte(Random(256));
Sekvenca restartuje sa svakim procesom i napreduje unutar njega, pa prvi dokument koji bilo koji proces šifruje deli svoj ključ fajla sa prvim dokumentom svakog drugog procesa koji radi isti build, drugi sa drugim, i tako dalje. Ključ fajla u R5, R6 i R7 uopšte ne zavisi od lozinke, pošto je lozinka samo umotava, što znači da svako ko može da reprodukuje sekvencu drži ključ bez znanja lozinke. AESV4 dodaje drugi kvar: isti ključ sa istim 8-bajtnim prefiksom i brojačem koji kreće iznova ponavlja GCM nonce-ove, što NIST SP 800-38D §8 izričito zabranjuje. Ponovljen GCM nonce pod jednim ključem otkriva XOR dva otvorena teksta i izlaže authentication podključ, pa oznake na koje se oslanja AESV4-GCM šifrovanje i PDF MAC token prestaju da znače bilo šta. Poverljivost i integritet odlaze istovremeno
Domet je uži nego što taj paragraf možda sugeriše. Windows buildovi nikada nisu bili pogođeni, jer Windows grana oduvek zove CryptGenRandom kroz advapi32 sa CRYPT_VERIFYCONTEXT i podiže izuzetak kad to ne uspe. Izložen je bio izlaz ne-Windows buildova starijih od 3.114.8, što u praksi znači Lazarus i Free Pascal aplikacije na Linux-u i macOS-u, još jedan unos na spisak Delphi naspram FPC zamki u PDFium buildovima
Zašto Randomize nikada nije bila prava popravka?
Zvati Randomize bi sakrilo simptom ne popravljajući izvor, jer je RandSeed 32-bitna vrednost i Randomize je izvodi iz sata. To ograničava broj mogućih ključnih tokova na 2^32, i približno znanje o tome kad je fajl pisan seče pretragu daleko ispod toga, što je ništa naspram 256-bitnog AES ključa. Materijal ključeva mora da dolazi iz kernel bazena entropije, pa AesGenerateRandomBytes u 3.114.8 čita /dev/urandom, vrti se preko kratkih čitanja, i podiže izuzetak ako bazen ne može da isporuči svaki traženi bajt:
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; // kvar ili neočekivani kraj stream-a
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');
Odbijanje je namerno, i poklapa se sa onim što je Windows grana oduvek radila kad CryptGenRandom nije dostupan. Neuspešno šifrovano čuvanje je incident koji primetite isti dan; uspešno čuvanje sa predvidivim ključevima je ono o kom saznate od nekog drugog. Prate dve praktične posledice. Minimalni kontejner ili chroot bez napunjene /dev sada pada na šifrovanju umesto da se tiho degradira, pa ga montirajte. I pošto izuzetak propagira iz TPdf.SaveAsEncrypted posle što je ciljni fajl otvoren sa fmCreate, ostaje prazan izlazni fajl koji Vaš error handler treba da obriše
Zašto round-trip testovi nikada nisu uhvatili defekt?
Round-trip test ne vidi konstantnu slučajnost, jer dešifrovanje povrati kakav god ključ fajla šifrovanje izabralo. Test šifruje dokument, otvori ga ponovo sa lozinkom, odmotava ključ iz /UE, i dešifruje svaki objekat; predvidiv ključ se odmotava i dešifruje baš kao slučajan, i GCM oznake verifikuju jer su računate tim istim ključem. Čak i test koji šifruje dvaput i tvrdi da se dva izlaza razlikuju prolazi, pošto drugi poziv u istom procesu vuče sledeće bajtove sekvence. Svojstvo koje je bitno, drugačiji ključ u svakom procesu, posmatrljivo je samo poređenjem izlaza preko procesa. Kad god isti kod i proizvodi i troši vrednost, testovi su slepi za cele klase defekata, i slučajnost je najčistiji primer
Kako testirati slučajnost ključeva preko procesa?
Pokrenite malu probu dvaput kao odvojene procese na ciljnoj platformi i uporedite izlaz. Proba ispod zove DeriveEncryptionKeys i štampa so sačuvanu u bajtovima 32 do 47 unosa /U. Ta se vrednost piše očigledno u svaki šifrovani fajl, pa njeno štampanje u CI logovima ne otkriva ništa, a dolazi iz istog generatora kao ključ fajla:
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 so
Hex := Hex + IntToHex(Keys.UEntry[I], 2);
WriteLn(Hex); // mora se razlikovati na svakom pokretanju
end.
Uvežite probu u build za svaki ne-Windows target: pokrenite je dvaput, oborite posao ako se dve linije poklope. Isto poređenje radi i na fajlovima koji su već u opticaju. Uzmite dva šifrovana PDF-a pisana različitim pokretanjima iste aplikacije, pročitajte /U string-ove iz njihovih Encrypt rečnika, i uporedite poslednjih 16 bajtova; identične soli identifikuju pogođeni build, a dokumente treba ponovo šifrovati iz njihovog otvorenog teksta sa 3.114.8 ili novijom da svaki dobije svež ključ fajla. Opšta navika je da se ne-Windows kodne putanje vežbaju na samoj platformi umesto da se veruje Windows pokretanju, isto razmišljanje iza libcurl timestamp backend-a za ne-Windows buildove
PDFiumPas je Delphi i Lazarus PDF komponenta građena na PDFium engine-u, sa AES-256, AES-GCM i PDF MAC tokenom implementiranim nativno u Pascal-u i materijalom ključeva vučenim iz generatora operativnog sistema na svakoj platformi. Detalji i preuzimanja su na stranici PDFium Delphi komponente