A 3.114.8-as verzió előtt a PDFiumPas a futtatókörnyezet Random függvényével termelte a PDF titkosítási kulcsanyagot a nem Windows célokon, és mivel senki sem hívta a Randomize-t, minden folyamat ugyanazt a bájtsorozatot állította elő. A Linux és macOS alatti Free Pascal buildek ezért futásról futásra azonos fájl-titkosítási kulcsokat, sókat, CBC IV-ket és AES-GCM nonce előtagokat írtak. A 3.114.8 ehelyett a /dev/urandom-ból olvas, és kivételt dob, ha nem tud
Maga a hiba egy négy soros ciklus. A hasznosabb tanulság az, miért maradt zölden az a tesztkészlet, amely AESV3-mal és AESV4-gyel, PDF MAC-kel és anélkül száznyi dokumentumot titkosít és visszafejt. A folyamatonkénti állandó véletlenszerűség minden, egyetlen folyamaton belül futó teszt elől láthatatlan, és pont így szokták megírni a titkosítási teszteket
Hol van szüksége a PDFiumPas-nak véletlen bájtokra?
A PDFiumPas titkosítási veremének minden véletlen bájta egyetlen eljárásból, az FPdfAes egység AesGenerateRandomBytes eljárásából érkezik, tehát egyetlen rossz forrás mindegyiket megfertőzi. Az ISO 32000-2 §7.6.4 szabványos biztonsági kezelője és az ISO/TS 32003 AESV4 kiterjesztése ezeken a helyeken fogyasztja ezeket a bájtokat:
- A 32 bájtos fájl-titkosítási kulcs, amelyet a DeriveEncryptionKeys dokumentumonként frissen generál, majd jelszóból származtatott kulcsokkal csomagol a /UE-be és az /OE-be
- Két 16 bájtos só, az egyik az /U utolsó 16 bájtjában, a másik az /O utolsó 16 bájtjában tárolódik, mindegyik 8 bájtos érvényesítő sóra és 8 bájtos kulcssóra oszlik
- Az /Perms mögötti nyílt szöveg 12-től 15-ig terjedő bájtjai, amelyeket az ISO 32000-2 véletlen adattal tölt meg, mielőtt a blokk a fájlkulcs alatt titkosításra kerülne
- Egy 16 bájtos CBC IV, amely minden titkosított string és stream elé kerül egy AESV3 dokumentumban
- Egy 8 bájtos nonce előtag AESV4 dokumentumoknál, amelyet nulláról induló, objektonkénti 4 bájtos számláló követ
- A 32 bájtos /KDFSalt és a MAC kulcs, ha az EnableIntegrityProtection be van kapcsolva
Miért termelt minden folyamat ugyanazt a kulcsot?
Az AesGenerateRandomBytes csak Windowson használta az operációs rendszer generátorát; minden másutt az RTL pszeudovéletlen generátorából töltötte a puffert, és az a generátor a RandSeed = 0 állapotból indul, hacsak a program nem hívja a Randomize-t. A ciklus feletti megjegyzés azt ígérte, hogy a generátor GetTickCount64-ből kap magot. Egyetlen kódsor sem tette meg soha, így a megjegyzés volt az egyetlen hely, ahol a mag egyáltalán létezett:
// Az AesGenerateRandomBytes nem Windows ága a 3.114.8 előtt
// (a felette lévő megjegyzés GetTickCount64 magot ígért, amelyet soha nem alkalmaztak)
P := PByte(Buffer);
for I := 0 to Count - 1 do
P[I] := Byte(Random(256));
A sorozat folyamatonként újrakezdődik, és folyamaton belül halad előre, így bármely folyamat első dokumentuma megosztja a fájlkulcsát minden más, ugyanazt a buildet futtató folyamat első dokumentumával, a második a másodikkal, és így tovább. Az R5, R6 és R7 fájlkulcsa a jelszótól egyáltalán nem függ, hiszen a jelszó csak becsomagolja, ami azt jelenti, hogy bárki, aki képes reprodukálni a sorozatot, jelszó nélkül birtokolja a kulcsot. Az AESV4 második kudarcot is ad: ugyanaz a kulcs ugyanazzal a 8 bájtos előtaggal és nulláról újrainduló számlálóval GCM nonce-ket ismétel, amit a NIST SP 800-38D §8 kategorikusan tilt. Egy kulcs alatt ismétlődő GCM nonce felfedi a két nyílt szöveg XOR-ját, és kitárja a hitelesítő alikulcsot, így a AESV4-GCM titkosításra és a PDF MAC tokenre támaszkodó címkék megszűnnek bármit is jelenteni. A bizalmasság és az integritás egyszerre megy tönkre
A hatókör szűkebb, mint amennyire az előző bekezdés sejtetni engedi. A Windows buildeket soha nem érintette, mert a Windows ág mindig is meghívta a CryptGenRandom-ot az advapi32-n keresztül CRYPT_VERIFYCONTEXT-tel, és kivételt dobott, ha ez elbukott. Ami kitett volt, az a 3.114.8-nál korábbi nem Windows buildek kimenete, ami a gyakorlatban Lazarus és Free Pascal alkalmazásokat jelent Linuxon és macOS-en, még egy bejegyzést a Delphi-FPC buktatók listájába PDFium buildeknél
Miért nem lehetett a Randomize a jó javítás?
A Randomize meghívása elrejtette volna a tünetet anélkül, hogy a forrást javította volna, mert a RandSeed egy 32 bites érték, és a Randomize az órából származtatja. Ez 2^32-re szorítja a lehetséges kulcsfolyamok számát, és ha nagyjából tudjuk, mikor íródott egy fájl, a keresés ettől jóval lejjebb szorul, ami semmi egy 256 bites AES kulcs mellett. A kulcsanyagnak a kernel entrópiakészletéből kell érkeznie, ezért a 3.114.8-ban az AesGenerateRandomBytes a /dev/urandom-ból olvas, rövid olvasásokon végigloopol, és kivételt dob, ha a készlet nem tud minden kért bájtot szállítani:
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; // hiba vagy váratlan streamvég
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');
Az elutasítás szándékos, és azt követi, amit a Windows ág mindig is tett, amikor a CryptGenRandom nem állt rendelkezésre. Egy elbukott titkosított mentés olyan incidens, amelyet még aznap észrevesz; egy sikeres, kiszámítható kulcsokkal írt mentés pedig olyan, amelyről mástól tud meg. Két gyakorlati következménye van. Egy minimális konténer vagy chroot, amelyben nincs feltöltött /dev, mostantól csendes romlás helyett elbukik a titkosítással, tehát mountolja fel. Mivel a kivétel a TPdf.SaveAsEncrypted-ből propagálódik ki azután, hogy a célfájl fmCreate-tel megnyílt, egy üres kimeneti fájl marad hátra, amelyet a hibakezelőjének törölnie kell
Miért nem csípte el soha a round-trip teszt?
Egy round-trip teszt nem látja az állandó véletlenszerűséget, mert a visszafejtés visszanyeri, amelyik fájlkulcsot a titkosítás választotta. A teszt titkosít egy dokumentumot, újra megnyitja a jelszóval, kicsomagolja a kulcsot az /UE-ből, és visszafejti az összes objektumot; egy kiszámítható kulcs pontosan olyan jól csomagol ki és fej vissza, mint egy véletlen, és a GCM címkék is átmentenek az ellenőrzésen, mert ugyanazzal a kulccsal számolták őket. Még az a teszt is átmegy, amely kétszer titkosít, és azt állítja, hogy a két kimenet eltér, hiszen ugyanazon folyamatban a második hívás a sorozat következő bájtjait húzza. A fontos tulajdonság — más kulcs minden folyamatban — csak folyamatok közötti kimenet-összehasonlításon figyelhető meg. Valahányszor ugyanaz a kód állítja elő és fogyasztja is az értéket, a tesztek teljes hibaosztályokra nézve vakok, és a véletlenszerűség a legtisztább példa erre
Hogyan tesztelheti a kulcsok véletlenszerűségét folyamatok között?
Futtasson egy kis szondát kétszer, külön folyamatokként a célplatformon, és hasonlítsa össze a kimenetet. Az alábbi szonda meghívja a DeriveEncryptionKeys-t, és kiírja az /U bejegyzés 32-től 47-ig terjedő bájtjaiban tárolt sót. Ez az érték szabad szemmel olvashatóan bekerül minden titkosított fájlba, tehát CI naplóba írni semmit sem fed fel, mégis ugyanabból a generátorból jön, mint a fájlkulcs:
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 bájtos hash + 16 bájtos só
Hex := Hex + IntToHex(Keys.UEntry[I], 2);
WriteLn(Hex); // minden futáson el kell térnie
end.
Kösse be a szondát a buildbe minden nem Windows célra: futtassa kétszer, és buktassa a feladatot, ha a két sor egyezik. Ugyanez az összehasonlítás a forgalomban lévő fájlokon is működik. Vegyen két titkosított PDF-et, amelyeket ugyanannak az alkalmazásnak különböző futásai írtak, olvassa ki az /U stringeket az Encrypt szótáraikból, és hasonlítsa össze az utolsó 16 bájtjukat; az azonos sók érintett buildet jeleznek, és a dokumentumokat 3.114.8-as vagy újabb verzióval a nyílt szövegükből újra titkosítani kell, hogy mindegyik friss fájlkulcsot kapjon. Az általános szokás az, hogy a nem Windows kódutakat magán a platformon gyakoroltatjuk, nem a Windows futásra hagyatkozva — ugyanez a gondolat áll a libcurl időbélyeg-backend mögött nem Windows buildeknél
A PDFiumPas egy Delphi és Lazarus PDF komponens, amely a PDFium motorra épül, AES-256-tal, AES-GCM-mel és a PDF MAC tokennel natívan Pascalban implementálva, és a kulcsanyagot minden platformon az operációs rendszer generátorából meríti. Részletek és letöltések a PDFium Delphi komponens oldalán