Teknik Makale

FPC'de PDF Şifreleme Anahtarları: PDFiumPas RNG Düzeltmesi

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ı
PDFiumPas şifreleme yığınındaki her rastgele bayt, FPdfAes içindeki AesGenerateRandomBytes'ten altı tüketiciye akar: /UE ve /OE içine sarılan 32 baytlık dosya şifreleme anahtarı, /U ve /O salt değerleri, /Perms dolgu baytları, AESV3 CBC IV'ü, AESV4 GCM nonce öneki ve KDF salt ile MAC anahtarı
Paylaşılan tek üreteç, kötü bir kaynağın anahtar malzemesini her yerde birden kirletmesi demektir; düzeltmenin her çağrı noktasına değil tek bir procedure'ye konmasının nedeni budur

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

Windows dışı FPC derlemelerinde PDFiumPas'taki tohumsuz RTL üreteci: RandSeed 0 ile her süreç aynı diziyi üretir, A sürecindeki birinci belge B sürecindeki birinci belgeyle aynı dosya anahtarını taşır ve AESV4, aynı anahtarın aynı önekle ve sıfırdan başlayan sayaçla buluşması nedeniyle GCM nonce'larını tekrarlar
Dosya anahtarı parolaya hiç bağlı olmadığı için diziyi yeniden üretebilen herkes anahtarı doğrudan ele geçirir; tekrarlanan GCM nonce'ları ise gizlilikle bütünlüğü birlikte yok eder

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:

PDFiumPas için süreçler-arası rastgelelik testi: SaltProbe programı DeriveEncryptionKeys çağırır ve /U'nun 32–47. baytlarının hex'ini yazdırır; iş, programı iki ayrı süreç olarak iki kez çalıştırır ve satırlar eşleştiğinde başarısız olur, dağıtılmış PDF'ler ise /U string'lerinin son 16 baytıyla karşılaştırılır
Sabit rastgelelik tek bir sürecin içinde görünmez, çünkü decryption, encryption'ın seçtiği anahtarı neyse onu geri kurtarır; önemli özellik yalnızca çıktıyı süreçler arasında karşılaştırarak gözlenebilir
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