Bir AES-256 revision 6 PDF, doğru olduğunu bildiğiniz bir şifreyi reddettiğinde alışılmış şüpheli, R6 sertleştirilmiş hashindeki (ISO 32000-2 Algorithm 2.B) tur sayacıdır. Delphi için PDFium Component, v2.57.2 öncesinde tam olarak bu hataya sahipti: TPdf.SaveAsEncrypted turları yanlış tabandan sayıyordu ve ürettiği dosyaların kabaca %30'u, FPDF_LoadDocument tarafından FPDF_ERR_PASSWORD ile reddediliyordu
Çirkin olan kısım aritmetik değil. Çirkin olan kısım, kusurun dosyayı şifreleyip sonra aynı kodla denetleyen her teste görünmez olması ve tek bir başarılı gidiş dönüşün hiçbir şey kanıtlamamasıydı. Bu yazı Algorithm 2.B'nin gerçekte ne istediğini, bir tur kaymasının nerede saklandığını, neden yalnızca zaman zaman başarısız olduğunu ve böylesi bir hatayı müşteriden önce yakalayan bir regresyon testinin nasıl kurulacağını anlatır
R6 hash sonlandırma kuralı gerçekte ne der?
Algorithm 2.B, en az 64 tur geçmeden durmaz ve durduğunda da en güncel AES çıktısı E'nin son baytı, tur sayısı eksi 32'den küçük ya da eşit olduğunda durur; tur sayısı ise hâlihazırda çalıştırılmış turların 1 tabanlı sayısıdır. N'inci turdan sonra N'ye karşılaştırırsınız. N − 1 değil, sonraki turun indeksi değil. Hata yüzeyinin tamamı o tek tanımdır
Algoritmanın gerisi mekaniktir. K'ye, şifrenin, tuzun ve (owner key için) 48 baytlık /U değerinin SHA-256'sı olarak başlarsınız. Her tur K1'i, şifre ‖ K ‖ UK'nin 64 kez tekrarı olarak kurar; onu, K'nin ilk 16 baytını anahtar, sonraki 16'yı IV yaparak dolgusuz AES-128-CBC ile şifreler; E'nin ilk 16 baytını big-endian 128 bitlik bir tamsayının mod 3'ü olarak alır ve sonraki K'yı üretmek için buna göre SHA-256, SHA-384 ya da SHA-512 seçer. Son hash, K'nin ilk 32 baytıdır. Onu ISO 32000-2 §7.6.4.3 tanımlar; veriye bağlı döngü uzunluğu bilinçlidir, çünkü şifre tahmini başına işi bir GPU kırıcısı için öngörülemez yapar
İşte döngünün doğru şekli; doğrudan yapıştırılacak bir uygulama değil illüstrasyon olarak yazıldı. BuildK1, AesCbcNoPad128, FirstBlockMod3 ve hash yardımcıları, kod tabanınızın zaten sahip olduğu ilkel neyse onun yerine geçer
// İllüstrasyon: Algorithm 2.B döngü iskeleti (ISO 32000-2, revision 6)
K := Sha256(Password + Salt + UK);
RoundNumber := 0; // şimdiye dek çalıştırılan turlar
repeat
Inc(RoundNumber); // bu tur 1, 2, 3 ...
K1 := BuildK1(Password, K, UK); // (Password || K || UK) 64 kez tekrarlı
E := AesCbcNoPad128(Copy(K, 0, 16), Copy(K, 16, 16), K1);
case FirstBlockMod3(E) of // E'nin ilk 16 baytı, big-endian, mod 3
0: K := Sha256(E);
1: K := Sha384(E);
2: K := Sha512(E);
end;
until (RoundNumber >= 64) and (E[High(E)] <= RoundNumber - 32);
Result := Copy(K, 0, 32);
Sonlandırma satırını sayacı göz önünde tutarak okuyun. Artış, gövdenin tepesinde durur; böylece until testi çalıştığında RoundNumber, tamamlanmış tur sayısını tam olarak tutar ve standardın tur sayısı derken kastettiği de budur
Bir tur kayması Delphi portunda nerede saklanır
PDFium Component hatası, döngünün tepesinde artışla birlikte −1'den başlayan bir sayacıydı; böylece until cümlesinde denetlenen değer her zaman çalıştırılmış tur sayısının bir eksiğiydi. İki şey aynı anda bozulur: 64 turluk asgari 65 olur ve bayt eşiği, standardın söylediğinden bir çentik sıkılaşı. Önem taşıyan durumlarda ikisi de çıkış noktasını tam olarak bir tur kaydırır
Aynı kaymanın birbirinden inandırıcı birkaç kılığı vardır. 0'dan başlayıp artışı gövdenin dibine, test artış öncesi değere göre çoktan yazılmışken koymak. 0 tabanlı bir for döngü indeksi kullanıp karşılaştırmada onu yeniden kullanmak. Döngü değişkeni ++den önce denetlenen bir C referansını çevirmek. Her biri kod incelemesinde doğal okunur ve her biri kusursuz rastgele görünen bir hash üretir; çünkü öyledir: hiçbir başka okuyucunun hesaplamadığı, farklı ve eşit derecede geçerli görünen bir SHA çıktısıdır sadece. FPdfEncrypt.pas içindeki düzeltilmiş CalculateR6Hash artık 1 tabanlı semantiği sabitleyen bir yorum taşıyor; tam da yanlış sürümün yanlış görünmemesi nedeniyle
Bir AES-256 R6 PDF neden yalnızca zaman zaman başarısız olur?
Başarısızlık olasılıksaldır; çünkü fazladan tur, sonlandıran bayt doğru eşik ile kaymış eşik arasındaki tek turluk pencereye düştüğünde sonucu değiştirir. Diğer her durumda iki sürüm de aynı turda çıkar ve aynı K'yı üretir; dosya sorunsuz açılır. PDFium Component ölçümünde 60 şifrelemeden 18'i (yaklaşık %30), standart bir uygulamayla ters düşen /U, /O, /UE ve /OE değerleri üretti; öbür %70 bayt baytına doğruydu
Her kayıt için taze rastgele tuzlar çekilir; böylece aynı belge ve aynı şifre bir çalıştırmada başarılı, sonrakinde başarısız olur. Bir destek masasından bakınca bu, kararsız bir depolama, bozuk bir yükleme ya da şifresini yanlış yazan bir kullanıcı gibi görünür. On dosyadan yedisi açarken kimse bir döngü sayacını suçlamayı düşünmez. Ürününüz, kendi kodunuzun şifrelediği PDF'lerde aralıklı "yanlış şifre" bildirimleri görüyorsa ve yalnızca revision 6'daysa bakılacak ilk yer burasıdır
Revision 5 bağışıktır; yinelemesiz tek bir SHA-256 geçişi kullanır. ISO/TS 32003 AESV4 yolu bağışık değildir: TPdfEncryptionRevision değeri erR7, şifre hash'leri için Algorithm 2.B'yi yeniden kullanır; böylece aynı düzeltme ISO/TS 32003 ile AES-256-GCM şifrelemesi ve PDF MAC bütünlüğü yazısında anlatılan GCM dosyalarını da kapsar
Öz tutarlılık tuzağı: gidiş dönüş testleri neden geçer
Kendi şifrelemeniz ve kendi doğrulamanız üzerinden bir gidiş dönüş daima başarılı olur; çünkü iki taraf da aynı yanlış döngüyü çalıştırır ve aynı yanlış hash'te anlaşır. Kusur yalnızca başkasının uygulamasına göre var olur; bu yüzden hakem bağımsız biri olmalıdır: pdfium.dll içindeki PDFium motoru, Acrobat, qpdf ya da yazmadığınız herhangi bir okuyucu
Bu, kripto komşusu belge kodu yazan herkes için rahatsız edici genel derstir. Bir hash hesaplayıp onu test edilen fonksiyonun çıktısıyla karşılaştıran unit testler, doğruluğu değil determinizmi kanıtlar. Standarddan ya da bir referans araçtan gelen known-answer vektörleri daha iyidir ve çapraz uygulama gidiş dönüşü, elde edebileceğiniz ground truth'a en yakın şeydir. PDFium Component burada şanslı bir konumdadır: şifreleme Pascal'da EncryptDocumentBytes tarafından yapılır; yüklemdeki şifre çözme ise PDFium'un kendi C++ security handler'ından geçer; böylece kaydedilen dosyayı TPdf ile yeniden açmak bir ayna değil, gerçekten bağımsız bir denetimdir
uses
SysUtils, PDFium, FPdfEncrypt;
procedure SaveR6(const Source, Target: string);
var
Pdf: TPdf;
Opts: TPdfEncryptOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := Source;
Pdf.Active := True;
Opts := TPdfEncryptOptions.Default;
Opts.UserPassword := 'userpw';
Opts.OwnerPassword := 'ownerpw';
Opts.Revision := erR6; // AESV3, Algorithm 2.B şifre hash'i
if not Pdf.SaveAsEncrypted(Target, Opts) then
raise Exception.Create('SaveAsEncrypted failed');
finally
Pdf.Free;
end;
end;
TPdfEncryptOptions.Default, bütün izinler açık ve meta veri şifreli şekilde revision 5'ten başlar; R6 istediğinizde Revisioni açıkça ayarlayın. Çıktı bir dosya yerine bir TStreame giderken TPdf.SaveAsEncryptedToStream aynı options kaydını alır. Şifrelerin options kaydında AnsiString olduğuna dikkat; ASCII dışı kimlik bilgileri geçiriyorsanız bu önemlidir. Onları kayda ulaşmadan önce normalize edin ve kodlayın
Olasılıksal bir şifreleme hatası nasıl regresyon testine bağlanır?
Çapraz uygulama gidiş dönüşünü, bir %30'luk başarısızlık oranının araya sığamayacağı kadar tekrarlayın. Tek bir çalıştırma, PDFium Component hatasını yaklaşık %70 oranında ıskalar. Her biri taze tuzlu yirmi çalıştırma, ıskalama oranını yaklaşık %0,08'e (0,7'nin 20. kuvveti) indirir; sevk edilen regresyon testinin bir kez değil 20 kez dönmesinin nedeni budur
procedure TEncryptTests.R6_EncryptedDocumentReopensWithUserPassword;
var
Pdf: TPdf;
I: Integer;
Target: string;
begin
Target := TempOutputPath('encrypt_r6_roundtrip.pdf');
for I := 1 to 20 do
begin
SaveR6(SamplePdf, Target); // her kayıtta taze tuzlar
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'userpw';
Pdf.FileName := Target;
Pdf.Active := True; // PDFium /U'yu bağımsız doğrular
AssertTrue('Run ' + IntToStr(I) + ' must reopen', Pdf.Active);
finally
Pdf.Free;
end;
end;
end;
Bunu, başarısız olması gereken yanlış bir şifre olan negatif durumla eşleştirin; böylece bozuk bir yükleyici pozitif testi boşça geçiremesin. Ve Lazarus ile derliyorsanız Lib\*.pas dosyalarına dokunduktan sonra test projesini çalıştırmadan önce kütüphane paketini yeniden derleyin; test derlemesi paketin önceden derlenmiş .ppu unit'lerini bağlar ve bayat object koduna karşı koşmak, gerçekte düzeltmediğiniz bir hatada iç rahatlatan bir yeşil verir. Tohumlama, aynı geçişte gözden geçirmeye değer ilgili bir tuzaktır: FPC Random'un sabit şifreleme anahtarları üretmesi, çıktının rastgele görünüp sessizce yanlış olduğu başka bir durumdur
FAQ
R6 tur sayacı hatası revision 5 ile şifrelenmiş dosyaları etkiler mi?
Hayır. Revision 5, şifreyi tek bir SHA-256 geçişiyle hash'ler ve yineleme sayısı yoktur; yanlış yapılacak bir tur sayacı da yoktur. Etkilenenler yalnızca revision 6 ve her ikisi de Algorithm 2.B kullanan ISO/TS 32003 AESV4 revisionıdır
Hatalı sürümün yazdığı bir dosya yine de açılabilir mi?
Kabaca %70'i normal açılır; çünkü hash'leri tesadüfen eşleşti. Geri kalanı hiçbir standart okuyucunun üretemediği /U ve /O değerleri taşır; şifre denetimi her yerde başarısız olur. Pratik çözüm, düz metin kaynaktan v2.57.2 ya da üstüyle yeniden şifrelemektir
Kendi şifre çözme kodum yerine neden PDFium'a karşı test edilir?
Kendi şifre çözücünüz, şifreleyicinizle aynı Algorithm 2.B uygulamasını paylaşır; yanlış bir tur sayısı sadeleşir ve her gidiş dönüş geçer. PDFium'un security handler'ı gibi bağımsız bir uygulama, sizinle ters düşebilecek tek denetimdir
Şifreleme, PDF MAC doğrulaması ve kayıt hattının gerisi, Delphi ve Lazarus için kaynak ve regresyon testleriyle birlikte Delphi ve C++Builder için PDFium Component içinde gelir