3.114.8 sürümünden önce PDFiumPas, Windows dışı hedeflerde PDF şifreleme anahtar malzemesini çalışma zamanı kütüphanesinin Random fonksiyonuyla üretiyordu ve hiçbir şey Randomize çağırmadığı için her süreç aynı bayt dizisini üretiyordu. Linux ve macOS'taki Free Pascal derlemeleri bu yüzden çalıştırmadan çalıştırmaya özdeş dosya şifreleme anahtarları, salt değerleri, CBC IV'leri ve AES-GCM nonce önekleri yazıyordu. 3.114.8 sürümü bunun yerine /dev/urandom okuyor ve okuyamadığında exception fırlatıyor
Kusurun kendisi dört satırlık bir döngü. Daha yararlı ders ise, AESV3 ve AESV4 ile, PDF MAC'li ve MAC'siz, yüzlerce belgeyi şifreleyip çözen bir test paketinin neden baştan sona yeşil kaldığı. Süreç başına sabit olan rastgelelik, tek bir sürecin içinde çalışan hiçbir teste görünmez ve şifreleme testleri de genellikle tam olarak böyle yazılır
PDFiumPas nerede rastgele bayta ihtiyaç duyar?
PDFiumPas şifreleme yığınındaki her rastgele bayt tek bir procedureden, FPdfAes unit'indeki AesGenerateRandomBytes'ten gelir; kötü bir kaynak hepsini birden kirletir. ISO 32000-2 §7.6.4'teki standart security handler ve ISO/TS 32003'teki AESV4 uzantısı o baytları şu yerlerde tüketir:
- 32 baytlık dosya şifreleme anahtarı, her belge için DeriveEncryptionKeys tarafından taze üretilir ve sonra paroladan türetilen anahtarlarla /UE ile /OE içine sarılır
- İki 16 baytlık salt: biri /U'nun son 16 baytında, biri /O'nun son 16 baytında saklanır; her biri 8 baytlık bir doğrulama salt'ı ile 8 baytlık bir anahtar salt'ına bölünür
- /Perms ardındaki düz metnin 12–15. baytları; ISO 32000-2, blok dosya anahtarıyla şifrelenmeden önce bu baytları rastgele veriyle doldurur
- AESV3 belgesindeki her şifreli string'in ve stream'in başına eklenen 16 baytlık CBC IV
- AESV4 belgeleri için 8 baytlık nonce öneki, ardından sıfırdan başlayan 4 baytlık nesne-başına sayaç
- EnableIntegrityProtection set edildiğinde 32 baytlık /KDFSalt ve MAC anahtarı
Her süreç neden aynı anahtarı üretti?
AesGenerateRandomBytes işletim sistemi üreticisini yalnızca Windows'ta kullanıyordu; diğer her yerde tamponu RTL pseudo-random üreteciyle dolduruyordu ve o üreteç, program Randomize çağırmadığı sürece RandSeed = 0'dan başlar. Döngünün üstündeki yorum, üretecin GetTickCount64'ten tohumlandığını söylüyordu. Hiçbir kod satırı bunu yapmadı; tohumun var olduğu tek yer o yorum oldu:
// 3.114.8 öncesi AesGenerateRandomBytes'in Windows dışı dalı
// (üstündeki yorum, hiç uygulanmayan bir GetTickCount64 tohumu vaat ediyordu)
P := PByte(Buffer);
for I := 0 to Count - 1 do
P[I] := Byte(Random(256));
Dizi her süreçte yeniden başlar ve süreç içinde ilerler; dolayısıyla herhangi bir sürecin şifrelediği ilk belge, aynı derlemeyi çalıştıran diğer her sürecin ilk belgesiyle dosya anahtarını paylaşır, ikincisi ikincisiyle, böylece gider. R5, R6 ve R7'deki dosya anahtarı parolaya hiç bağlı değildir, çünkü parola yalnızca onu sarar; yani diziyi yeniden üretebilen herkes, parola bilmeden anahtarı elinde tutar. AESV4 ikinci bir başarısızlık ekler: aynı anahtar, aynı 8 baytlık önekle ve sıfırdan yeniden başlayan bir sayaçla GCM nonce'larını tekrarlar ve NIST SP 800-38D §8 bunu koşulsuz yasaklar. Tek anahtar altında tekrarlanan bir GCM nonce, iki düz metnin XOR'unu ifşa eder ve kimlik doğrulama alt anahtarını açığa çıkarır; AESV4-GCM şifreleme ve PDF MAC token'ının güvendiği tag'ler anlam taşımayı bırakır. Gizlilik ve bütünlük aynı anda gider
Kapsam, o paragrafın imâ ettiğinden dardır. Windows derlemeleri hiç etkilenmedi, çünkü Windows dalı her zaman CryptGenRandom'u advapi32 üzerinden CRYPT_VERIFYCONTEXT ile çağırdı ve başarısız olduğunda exception fırlattı. Açığa çıkan şey, 3.114.8 öncesi Windows dışı derlemelerin çıktısıydı; pratikte bu, Linux ve macOS'taki Lazarus ve Free Pascal uygulamaları demektir ve PDFium derlemelerinde Delphi ile FPC arasındaki tuzaklar listesine bir madde daha ekler
Randomize neden hiçbir zaman doğru çözüm olmadı?
Randomize çağırmak kaynağı düzeltmeden belirtiyi saklar durumda olurdu, çünkü RandSeed 32 bitlik bir değerdir ve Randomize onu saatten türetir. Bu, olası anahtar akışı sayısını 2^32 ile sınırlar; bir dosyanın kabaca ne zaman yazıldığını bilmek aramayı bunun çok altına indirir ki bu da 256 bitlik bir AES anahtarının yanında hiçtir. Anahtar malzemesi çekirdeğin entropy havuzundan gelmek zorundadır; 3.114.8'deki AesGenerateRandomBytes bu yüzden /dev/urandom'u okur, kısa okumaların üzerinden döngü kurar ve havuz istenen her baytı teslim edemezse exception fırlatır:
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; // başarısızlık ya da beklenmeyen akış sonu
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');
Reddetmek bilinçli bir tercih ve Windows dalının CryptGenRandom mevcut olmadığında hep yaptığı şeyin aynısı. Başarısız bir şifreli kaydetme, aynı gün fark ettiğiniz bir olaydır; öngörülebilir anahtarlarla başarılı bir kaydetme ise başkasından öğrendiğiniz olaydır. İki pratik sonuç doğar. /Dev'i doldurulmamış minimal bir container ya da chroot artık sessizce kötüleşmek yerine şifrelemeyi başarısız yapar; mount edin. Ve exception, hedef dosya fmCreate ile açıldıktan sonra TPdf.SaveAsEncrypted dışına yayıldığı için, geriye hata handler'ınızın silmesine borç bir boş çıktı dosyası kalır
Round-trip testleri bunu neden hiç yakalayamadı?
Round-trip testi sabit rastgeleliği göremez, çünkü decryption, encryption'ın seçtiği dosya anahtarını neyse onu geri kurtarır. Test bir belgeyi şifreler, parolayla yeniden açar, anahtarı /UE'den çözer ve her nesneyi decrypt eder; öngörülebilir bir anahtar, rastgele bir anahtar kadar iyi unwrap edilir ve decrypt edilir, GCM tag'leri de doğrulanır çünkü aynı anahtarla hesaplanmışlardır. İki kez şifreleyip iki çıktının farklı olduğunu doğrulayan bir test bile geçer, çünkü aynı süreçteki ikinci çağrı dizinin sonraki baytlarını çeker. Önemli özellik — her süreçte farklı bir anahtar — yalnızca çıktıyı süreçler arasında karşılaştırarak gözlenebilir. Aynı kod hem bir değeri üretip hem tükettiğinde testler bütün hata sınıflarına karşı kördür ve rastgelelik bunun en saf örneğidir
Anahtar rastgeleliğini süreçler arasında nasıl test edersiniz?
Hedef platformda küçük bir sonda programı iki ayrı süreç olarak iki kez çalıştırın ve çıktıları karşılaştırın. Aşağıdaki sondacı, DeriveEncryptionKeys çağırır ve /U girdisinin 32–47. baytlarında saklanan salt değerini yazdırır. O değer her şifreli dosyaya açıkça yazılır; CI günlüklerinde yazdırmak hiçbir şey ifşa etmez ama dosya anahtarıyla aynı üreteçten gelir:
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 baytlık hash + 16 baytlık salt
Hex := Hex + IntToHex(Keys.UEntry[I], 2);
WriteLn(Hex); // her çalıştırmada farklı olmalı
end.
Sondacı her Windows dışı hedef için derlemeye bağlayın: iki kez çalıştırın, iki satır eşleşirse işi düşürün. Aynı karşılaştırma, hâlihazırda dolaşımda olan dosyalar üzerinde de işler. Aynı uygulamanın farklı çalıştırmalarının yazdığı iki şifreli PDF alın, Encrypt sözlüklerinden /U string'lerini okuyun ve son 16 baytı karşılaştırın; özdeş salt değerleri etkilenmiş bir derlemeyi işaret eder ve belgeler, her biri taze bir dosya anahtarı alsın diye düz metinlerinden 3.114.8 ya da üzeriyle yeniden şifrelenmelidir. Genel alışkanlık, Windows dışı kod yollarını Windows çalıştırmasına güvenmek yerine platformun üzerinde çalıştırmak olsun; Windows dışı derlemeler için libcurl timestamp backend'i arkasındaki akıl yürütme de aynıdır
PDFiumPas, PDFium motoru üzerine kurulmuş bir Delphi ve Lazarus PDF component'idir; AES-256, AES-GCM ve PDF MAC token'ı Pascal'da doğal olarak uygulanmıştır ve anahtar malzemesi her platformda işletim sistemi üreteci çekilir. Ayrıntılar ve indirmeler PDFium Delphi component sayfasında