Teknik Makale

PDF Şifreleme Anahtarları Neden /dev/urandom İster: FPC

PDF şifreleme anahtar malzemesini çalışma zamanı kütüphanesinin Random işleviyle üretmek Free Pascal'da bir tuzaaktır: birileri Randomize çağırmadıkça her süreç aynı bayt dizisini üretir; o da Linux ve macOS derlemelerinin, çalıştırmadan çalıştırmaya özdeş dosya şifreleme anahtarları, salt'lar, CBC IV'leri ve AES-GCM nonce önekleri yazmasına yol açar. PDFiumPas, Windows olmayan hedeflerde bunun yerine /dev/urandom'dan okur ve okuyamazsa istisna fırlatır

Tuzak kendi başına dört satırlık bir döngüye sığar. Daha yararlı ders, şifreleme test takımının — AESV3 ve AESV4, PDF MAC'li ve MAC'siz — nasıl olup da tam üzerinde yeşil kalabildiğidir. Süreç başına sabit rastgelelik, tek bir sürecin içinde çalışan her test için görünmezdir ve şifreleme test takımları genelde tam olarak böyle örgütlenir

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ı
Every random byte in the PDFiumPas encryption stack flows from AesGenerateRandomBytes in FPdfAes into six consumers: the 32-byte file encryption key wrapped into /UE and /OE, the /U and /O salts, /Perms padding bytes, the AESV3 CBC IV, the AESV4 GCM nonce prefix, and the KDF salt and MAC key
Tek bir ortak üretici, kötü bir kaynağın anahtar malzemesini her yerde birden kirletebilmesi demektir; iyi bir kaynak da her çağrı yerini aynı anda kapsar

Her süreç aynı anahtarı neden üretsin?

RTL sözde rastgele üreticisi, program Randomize çağırmadıkça RandSeed = 0 ile başlar; dolayısıyla anahtar tamponunu onunla dolduran bir Windows olmayan dal, her süreçte aynı diziyi alır. Üreticinin GetTickCount64 ile tohumlandığını söyleyen bir yorum, aslında hiçbir satır bunu yapmıyorken işe yaramaz; tohum, yalnızca yorumun içinde var olur:

// Tuzak: anahtar kaynağı olarak tohumlanmamış RTL üreticisi
// (GetTickCount64 tohumu vaat eden bir yorum, tohumlama yapmaz)
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

Unseeded RTL generator on non-Windows FPC builds: with RandSeed 0 every process emits the same sequence, so document one in process A carries the same file key as document one in process B, and AESV4 repeats GCM nonces because the same key meets the same prefix with the counter restarting at zero
Dosya anahtarı şifreye hiç bağlı olmadığı için diziyi yeniden üretebilen biri anahtarı açık açık elde eder; tekrarlanan GCM nonce'ları ise gizlilikle bütünlüğü birlikte götürür

Kapsam, o paragrafın çağrıştırdığından dardır. Windows'ta soru doğmaz, çünkü Windows dalı CryptGenRandom'u advapi32 üzerinden CRYPT_VERIFYCONTEXT ile çağırır ve başarısızlığında istisna fırlatır. Tuzak, Windows olmayan hedeflere özgüdür; pratikte bu da 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 çözüm değildir?

Randomize çağırmak kaynağı düzeltmeden belirtiyi gizler, çünkü RandSeed 32 bitlik bir değerdir ve Randomize onu saatten türetir. Bu, olası anahtar dizisi sayısını 2^32 ile sınırlar; bir dosyanın kabaca ne zaman yazıldığını bilmek aramayı bunun da çok altına indirir ki 256 bitlik bir AES anahtarının yanında bu hiçbir şeydir. Anahtar malzemesi çekirdeğin entropi havuzundan gelmelidir; o yüzden AesGenerateRandomBytes /dev/urandom'dan okur, kısa okumalar üzerinde döngü kurar ve havuz istenen her baytı teslim edemezse istisna 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');

Ret etmek bilinçlidir ve Windows dalının CryptGenRandom olmadığında yaptığıyla aynıdır. Başarısız bir şifreli kayıt, aynı gün fark edeceğiniz bir olaydır; öngörülebilir anahtarlarla başarılı bir kayıt ise başkalarından öğreneceğiniz bir olaydır. İki pratik sonuç çıkar: /dev'i dolu olmayan asgari bir konteyner ya da chroot, şifrelemeyi sessizce bozmak yerine başarısız kılar; o yüzden onu mount edin. Ve istisna, hedef dosya fmCreate ile açıldıktan sonra TPdf.SaveAsEncrypted dışına yayıldığı için, hata işleyicinizin silmesi için boş bir çıktı dosyası geride kalır

Gidiş-dönüş testleri neden yakalamaz?

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.

Probe'u, Windows olmayan her hedef için derlemeye bağlayın: iki kez çalıştırın, iki satır eşleşirse işi başarısız bırakın. Aynı karşılaştırma, hâlihazırda dolaşımda olan dosyalarda da çalışır. Aynı uygulamanın farklı çalıştırmalarının yazdığı iki şifreli PDF alın, Encrypt sözlüklerinden /U stringlerini okuyun ve son 16 baytı karşılaştırın; özdeş salt'lar, anahtar kaynağının öngörülebilir olduğu anlamına gelir ve o belgelerin her biri taze bir dosya anahtarı alması için düz metinlerinden yeniden şifrelenmesi gerekir. Genel alışkanlık şu olmalıdır: Windows çalıştırmasına güvenmek yerine Windows olmayan kod yollarını platformun kendisinde çalıştırın; Windows olmayan derlemeler için libcurl zaman damgası arka ucunun arkasındaki akıl yürütme de budur

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