Före version 3.114.8 genererade PDFiumPas nyckelmaterial för PDF-kryptering på icke-Windows-mål med runtime-bibliotekets Random-funktion, och eftersom ingenting anropade Randomize producerade varje process samma bytesekvens. Free Pascal-byggen på Linux och macOS skrev därför identiska filkrypteringsnycklar, salter, CBC-IV:er och AES-GCM-nonceprefix körning efter körning. Version 3.114.8 läser i stället /dev/urandom och kastar ett undantag när den inte kan
Defekten själv är en loop på fyra rader. Den mer användbara lärdomen är varför en testsvit som krypterar och dekrypterar hundratals dokument, med AESV3 och AESV4, med och utan PDF-MAC, höll sig grön hela tiden. Slump som är konstant per process är osynlig för varje test som körs inuti en process, och det är exakt hur krypteringstester vanligen skrivs
Var behöver PDFiumPas slumpmässiga byte?
Varje slumpmässig byte i PDFiumPas krypteringsstack kommer från en enda procedur, AesGenerateRandomBytes i enheten FPdfAes, så en dålig källa kontaminerar allt. Den standardiserade säkerhetshanteraren i ISO 32000-2 §7.6.4 och AESV4-utvidgningen i ISO/TS 32003 konsumerar dessa byte på följande ställen:
- Den 32 byte stora filkrypteringsnyckeln, genererad färsk av DeriveEncryptionKeys för varje dokument och därefter insvept i /UE och /OE under lösenordshärledda nycklar
- Två salter på 16 byte, ett lagrat i de sista 16 bytena av /U och ett i de sista 16 bytena av /O, vart och ett delat i ett valideringssalt på 8 byte och ett nyckelsalt på 8 byte
- Byte 12 till 15 i klartexten bakom /Perms, som ISO 32000-2 fyller med slumpdata innan blocket krypteras under filnyckeln
- En CBC-IV på 16 byte som föregår varje krypterad sträng och ström i ett AESV3-dokument
- Ett nonceprefix på 8 byte för AESV4-dokument, följt av en per objekt räknare på 4 byte som börjar på noll
- /KDFSalt på 32 byte och MAC-nyckeln när EnableIntegrityProtection är satt
Varför producerade varje process samma nyckel?
AesGenerateRandomBytes använde operativsystemets generator bara på Windows; överallt annars fyllde den bufferten från RTL:s pseudoslumpgenerator, och den generatorn startar från RandSeed = 0 om inte programmet anropar Randomize. Kommentaren ovanför loopen hävdade att generatorn seedades från GetTickCount64. Ingen rad kod gjorde någonsin det, vilket gjorde kommentaren till den enda plats där själva seedet fanns:
// Icke-Windows-grenen av AesGenerateRandomBytes före 3.114.8
// (kommentaren ovanför lovade ett GetTickCount64-seed som aldrig tillämpades)
P := PByte(Buffer);
for I := 0 to Count - 1 do
P[I] := Byte(Random(256));
Sekvensen börjar om med varje process och går framåt inom den, så det första dokument som en process krypterar delar sin filnyckel med det första dokumentet i varje annan process som kör samma bygge, det andra med det andra, och så vidare. Filnyckeln i R5, R6 och R7 beror inte alls på lösenordet, eftersom lösenordet bara sveper in den, vilket betyder att vem som helst som kan reproducera sekvensen håller nyckeln utan att känna ett lösenord. AESV4 lägger till ett andra fel: samma nyckel med samma 8-byte-prefix och en räknare som börjar om på noll upprepar GCM-nonces, vilket NIST SP 800-38D §8 förbjuder rakt ut. En upprepad GCM-nonce under en nyckel avslöjar XOR av de två klartexterna och exponerar autentiseringsundernyckeln, så de taggar som AESV4-GCM-kryptering och PDF-MAC-token litar på slutar betyda någonting. Konfidentialitet och integritet går på samma gång
Omfattningen är smalare än det stycket kan låta. Windows-byggen drabbades aldrig, för Windows-grenen har alltid anropat CryptGenRandom genom advapi32 med CRYPT_VERIFYCONTEXT och kastat när det misslyckades. Det som exponerades var utdata från icke-Windows-byggen äldre än 3.114.8, vilket i praktiken betyder Lazarus- och Free Pascal-applikationer på Linux och macOS, ytterligare en post i listan över Delphi kontra FPC-fällor i PDFium-byggen
Varför var Randomize aldrig den rätta fixen?
Ett anrop av Randomize skulle ha gömt symptomet utan att laga källan, för RandSeed är ett 32-bitarsvärde och Randomize härleder det från klockan. Det takar antalet möjliga nyckelströmmar vid 2^32, och att veta ungefär när en fil skrevs skär sökningen långt under det, vilket inte är någonting bredvid en AES-nyckel på 256 bitar. Nyckelmaterial måste komma från kärnans entropipool, så AesGenerateRandomBytes i 3.114.8 läser /dev/urandom, loopar över korta läsningar och kastar om poolen inte kan leverera varje begärt byte:
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; // fel eller oväntat slut på strömmen
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');
Att vägra är medvetet, och det stämmer med vad Windows-grenen alltid gjort när CryptGenRandom inte finns. En misslyckad krypterad sparning är en incident du märker samma dag; en lyckad sparning med förutsägbara nycklar är en du får höra om av någon annan. Två praktiska konsekvenser följer. En minimal container eller chroot utan en befolkad /dev misslyckas nu med kryptering i stället för att tysta degradera, så montera den. Och eftersom undantaget fortplantar sig ut ur TPdf.SaveAsEncrypted efter att målfilen öppnats med fmCreate lämnas en tom utdatafil kvar för din felhanterare att radera
Varför fångade rundturstester aldrig felet?
Ett rundturstest kan inte se konstant slump, för dekrypteringen återvinner vilken filnyckel krypteringen än valde. Testet krypterar ett dokument, öppnar det igen med lösenordet, vecker upp nyckeln ur /UE och dekrypterar varje objekt; en förutsägbar nyckel veckas upp och dekrypterar precis lika bra som en slumpmässig, och GCM-taggarna verifieras för de räknades fram med samma nyckel. Även ett test som krypterar två gånger och assertar att de två utdatorna skiljer sig går igenom, eftersom det andra anropet i samma process drar nästa byte av sekvensen. Egenskapen som spelar roll, en annan nyckel i varje process, är bara observerbar genom att jämföra utdata över processer. Närhelst samma kod både producerar och konsumerar ett värde är testerna blinda för hela klasser av defekter, och slump är det renaste exemplet
Hur kan du testa nyckelslumpen över processer?
Kör en liten sond två gånger som separata processer på målplattformen och jämför utdatan. Sonden nedan anropar DeriveEncryptionKeys och skriver ut saltet som lagras i byte 32 till 47 av /U-posten. Det värdet skrivs öppet in i varje krypterad fil, så att skriva ut det i CI-loggar avslöjar ingenting, men det kommer från samma generator som filnyckeln:
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-byte hash + 16-byte salt
Hex := Hex + IntToHex(Keys.UEntry[I], 2);
WriteLn(Hex); // måste skilja sig vid varje körning
end.
Koppla in sonden i bygget för varje icke-Windows-mål: kör den två gånger och låt jobbet misslyckas om de två raderna matchar. Samma jämförelse fungerar på filer som redan är i omlopp. Ta två krypterade PDF:er skrivna av olika körningar av samma applikation, läs /U-strängarna ur deras Encrypt-ordböcker och jämför de sista 16 bytena; identiska salter identifierar ett drabbat bygge, och dokumenten bör krypteras om från sin klartext med 3.114.8 eller senare så att var och en får en färsk filnyckel. Den allmänna vanan är att öva upp icke-Windows-kodvägar på plattformen själv i stället för att lita på Windows-körningen, samma resonemang som ligger bakom libcurl tidsstämpel-backend för icke-Windows-byggen
PDFiumPas är en PDF-komponent för Delphi och Lazarus byggd på PDFium-motorn, med AES-256, AES-GCM och PDF-MAC-token implementerade nativt i Pascal och nyckelmaterial hämtat från operativsystemets generator på alla plattformar. Detaljer och nedladdningar finns på sidan för PDFium Delphi-komponenten