Aynı fatura kodunun bir saniye arayla yazdığı iki PDF neredeyse hiçbir zaman aynı dosya değildir. İkili karşılaştırma önce Info sözlüğü içinde takılır; orada /CreationDate bir kaymıştır, sonra XMP paketinde, sonra trailer /ID'de. AES-256'yı açın ve karşılaştırma neredeyse her akışta çöker; çünkü dosya şifreleme anahtarının kendisi yeniden üretilmiştir. Karşılaştırmadan önce tarihleri maskelemek ilk iki farkı onarır, öbürlerinin hiçbirini. Bir altın-dosya testi, yalnızca yazıcının belgenin dışından aldığı her değer okunduğu noktada sabitlendiğinde güvenilir olur
Hiçbir zaman belgeden gelmemiş dört değer
HotPDF Delphi Component, standartlara uyan herhangi bir PDF yazıcısı gibi dört tür değeri sayfa içeriğinin dışından doldurur: saat, belge tanımlayıcı, kriptografik rastgele kaynak ve sözlük girdilerinin bellekteki sırası. ISO 32000-1'in her biri için bir gerekçesi vardır
- Saat. Info sözlüğü,
/CreationDateile/ModDate'i (ISO 32000-1 §14.3.3, Tablo 317) saat dilimi sonekli (§7.9.4)D:YYYYMMDDHHmmSSdizgileri olarak taşır ve XMP paketi aynı anıxmp:CreateDateilexmp:ModifyDateolarak yineler. Hepsi, bileşenin kurulduğu andaNow'dan aldığı tek bir oluşturma zaman damgasından işlenir - Tanımlayıcı. Trailer
/IDdizisi (§14.4) kalıcı bir tanımlayıcı ile bir değişiklik tanımlayıcısı tutar. Varsayılan olarak ilki, dosya adının milisaniyesine kadar geçerli zamanla birleşiminin MD5'idir; ikincisi o değeri sistem tick sayısıyla birleşimden yeniden hash'ler - Rastgele baytlar. Bir AES-256 belgesi, 32 baytlık bir dosya şifreleme anahtarı,
/Uiçin 8 baytlık bir doğrulama tuzu ve 8 baytlık bir anahtar tuzu,/Oiçin başka bir çift,/Permsiçinde dört dolgu baytı ve her şifreli dizgi ile akışın önünde 16 baytlık bir başlatma vektörü ister. ISO 32000-2 §7.6.4.4.7 tuzların rastgele olmasını ister. RC4 ile AES-128 anahtarlarını parolalardan ve ilk/IDelemanından türetir (ISO 32000-1 §7.6.3.3); bu yüzden tek başına taze bir tanımlayıcı, tüm dosyayı yeniden anahtarlama için yeterlidir - Sıra. Bir PDF sözlüğü sırasız bir eşlemedir (§7.3.7). Bellekteki listesini dolaşan bir yazıcı, anahtarları kodun tesadüfen eklediği sırada yazar; böylece aynı kaynak sözlüğünü farklı sırayla kuran iki kod yolu, yasal ama dizgisel olarak farklı bir dosya üretir
Geri kalan her şey zaten girdiden gelir: içerik akışları, sıkıştırılmış görüntü verisi ve çapraz başvuru ofsetleri aynı çizim çağrıları için tekrar eder. Rastgele görünen tek alan bile rastgele değildir: gömülü bir yazı tipi alt kümesinin altı harfli öneki (ISO 32000-1 §9.6.4, ABCDEF+Arial'in ABCDEF+ kısmı) alt kümenin tuttuğu glif kümesinden hesaplanır; aynı metin her çalıştırmada aynı öneki verir. Gürültü üst veride ve güvenlik katmanındadır; tek bir özelliğin onu kaldırabilmesinin nedeni budur
ReproducibleOutput neyi sabitler
BeginDoc'tan önce ReproducibleOutput := True yapmak, dört kaynağın her birini, aksi hâlde saate ya da rastgele üretece uzanan aynı kod yollarında sabit bir değerle değiştirir. Arkasından temizlik geçişi yoktur. Özellik varsayılan olarak False'tur ve kütüphanede onu sizin için açan hiçbir şey yoktur
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'golden-invoice.pdf';
Pdf.ReproducibleOutput := True; // BeginDoc'tan önce ayarlanmalı
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-0042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Bayrak altında BeginDoc, oluşturma zaman damgasını 1 Ocak 2026, yerel saatle 00:00'a çeker ve hem Info tarihleri hem XMP tarihleri o tek değerden işlendiği için tek bir atama hepsini kapsar. Aynı dal, iç belge tanımlayıcısını dosya adı ve saat yerine sabit bir dizgiden tohumlar. Dosya yazılırken trailer /ID, her D: tarih dizgisinin basamakları önce sıfırlanmış, tüm nesne grafiğinin kurallı bir çıktısı üzerinden MD5 olarak hesaplanır; böylece bir zaman damgası hash'e sızmış olamaz. /ID'nin her iki elemanı da o değeri alır. BeginDoc'u hiç çağırmadan şifrelediğiniz bir yüklü belge (LoadFromFile'dan sonra ActivateProtection kurun) aynı içerikten türetilmiş tanımlayıcıyı alır
Rastgele baytlar sabitlerle değiştirilir. AES-256 dosya anahtarı, dört tuzun tamamı ve /Perms dolgusu $5A ile doldurulur ve her CBC başlatma vektörü — AES-128 dizgi ve akışları da AES-256 gibi — I baytı için 14 * (1 + I) örüntüsüne döner. O örüntü kasıtlı olarak sıfır değildir. Tümü-sıfır bir IV, CBC için ders kitabındaki zayıf seçimdir ve bir regresyon paketinin onu normalleştirmeye işi yoktur. Anahtar, tuzlar ve vektörler sabitlenince /U, /UE, /O, /OE, /Perms ve her şifreli akış ikinci çalıştırmada aynı çıkar
Son olarak SaveToStream, yeniden üretilebilir bayrak kurulduğunda DeterministicDictionaryOrder'ı açar ve bunu linearized, object-stream ve klasik düzenler arasında seçim yapmadan önce yapar; böylece LinearizeOutput ya da UseObjectStreams ile üretilen çıktı da sıralanır. Anahtarlar ham baytlarına göre sıralanır, bir anahtar öbürünün önekiyse önce gelir ve eşitliği özgün konum kırar. O sıralama, bir PDF'i elle düzenleyip sonra onarmak yazısında anlatılan tanısal yazıcının kullandığı sıralamadır; yeniden üretilebilir bayrak sıralamayı ödünç alır, o yazıcının düz-metin düzeninin hiçbirini almaz. Yalnızca DeterministicDictionaryOrder'ı açmak yarım önlemdir. Anahtarlar hizalanır ama Info tarihleri ve her iki trailer /ID dizisi hâlâ saati taşır; iki kayıt aynı dört yerde farklı kalmayı sürdürür
Şansla geçemeyecek bir test
Tarih, BeginDoc içinde sabitlenir; çünkü kurucu, çağıranın özelliği ayarlamasına fırsat kalmadan koşar. Sıradan akış Create, sonra ReproducibleOutput := True, sonra BeginDoc'tir ve bayrağı son değeriyle gören yalnızca sonuncusudur. Bu sıralamanın test tasarımı için doğrudan bir sonucu vardır. Aynı duvar-saati saniyesine düşen iki kayıt aynı D: dizgisini tesadüfen yazar; böylece hızlı bir test, tarih sabitlenmiş olsun olmasın geçer, sonra aylar sonra daha yavaş bir derleme ajanında düşer
Çözüm ucuzdur: iki kayıt arasında 1100 ms bekleyin ki zaman damgasının bir saniye sınırını geçtiği garanti olsun; durumu düz, AES-128 ve AES-256 çıktıları için, şifreli olanlarda gerçek parolalarla çalıştırın ve çıplak true/false yerine ilk farklı bayt ofsetini raporlayın. Bir ofset tek bir nesneyi gösterir. Bir boole tüm dosyayı gösterir
uses
System.SysUtils, System.IOUtils, HPDFDoc;
function SaveOnce(const Target: string; Encrypt: Boolean;
KeyType: THPDFKeyType): TBytes;
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := Target;
Pdf.ReproducibleOutput := True;
Pdf.VerifySavedOutput := True; // EndDoc'tan hemen sonra katı yeniden yükleme
if Encrypt then
begin
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.CryptKeyLength := KeyType; // aes128 ya da aes256
Pdf.ActivateProtection := True;
end;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'reproducible save');
Pdf.EndDoc;
finally
Pdf.Free;
end;
Result := TFile.ReadAllBytes(Target);
end;
// Kendi yardımcınız: eşitken -1, değilse ilk farklı ofset
function FirstDifference(const A, B: TBytes): Integer;
var
I, N: Integer;
begin
N := Length(A);
if Length(B) < N then
N := Length(B);
for I := 0 to N - 1 do
if A[I] <> B[I] then
Exit(I);
if Length(A) <> Length(B) then
Result := N
else
Result := -1;
end;
procedure CheckReproducible(Encrypt: Boolean; KeyType: THPDFKeyType);
var
A, B: TBytes;
Offset: Integer;
begin
A := SaveOnce('run-a.pdf', Encrypt, KeyType);
Sleep(1100); // farklı bir zaman damgası saniyesini zorlar
B := SaveOnce('run-b.pdf', Encrypt, KeyType);
Offset := FirstDifference(A, B);
if Offset >= 0 then
raise Exception.CreateFmt('Saves differ at byte offset %d', [Offset]);
end;
CheckReproducible(False, aes128), CheckReproducible(True, aes128) ve CheckReproducible(True, aes256) çağırın; anahtar tipi düz çalıştırmada yok sayılır. VerifySavedOutput bu testteki yerini hak eder. Açıkken EndDoc, yazdığı dosyayı çapraz başvuru kurtarması devre dışı, taze bir THotPDF örneğiyle — belge korunuyorsa kullanıcı parolasıyla — yeniden yükler ve o yükleme başarısız olursa istisna oluşturur. Çıktıyı aynı anda hem kararlı hem okunmaz yapan bir değişiklik, yeşil bir diff'ten zarif biçimde geçer; yeniden yükleme onu durdurur
Yeniden üretilebilir şifreli bir PDF hâlâ güvenli mi?
Hayır. ReproducibleOutput altında şifrelenen bir belgenin gerçek bir koruması yoktur ve bayrak, test dizininden çıkan her şey için kapalı olmalıdır. AES-256 dosya anahtarı, otuz iki bayt $5A'dır. Parola hâlâ /UE ile /OE sarmalayıcılarını kapılar ama onların sardığı yayımlanmış bir sabittir; bilen herkes her içerik akışını parolasız çözebilir. Sabit tuzlar ayrıca, aynı parolanın iki dosyada aynı /U dizgisini üretmesini önlemek için ISO 32000-2 §7.6.4.4.7'nin güvendiği dosya-başına eşsizliği kaldırır. AES-256 kurulumu yazısı, rastgele kaynak sağlamken şifreleme özelliklerinin ne sağladığını anlatır; yeniden üretilebilir bayrak altında bunların hiçbiri geçerli değildir. Özelliği, üretim kod yolunun ulaşabileceği bir ayar değil, yalnızca-test yapılandırma değerinin arkasına koyun
AES-GCM ayrı ele alınır. CryptKeyLength := aesgcm iken (yalnızca PDF 2.0), bileşen 8 baytlık nonce önekini ReproducibleOutput ne derse desin sistem rastgele kaynağından çeker ve sonra her şifreli dizgi ya da akış için yukarı doğru sayar. Doğru davranış budur. Aynı anahtar altında bir nonce tekrarladığında GCM fena başarısız olur: iki şifreli metin, düz metinlerinin XOR'unu sızdırır ve bir saldırgan, kimlik doğrulama etiketleri sahtelemeye yetecek kadarını kurtarabilir. Sabitlenmiş anahtar artı sabitlenmiş nonce, paketin ürettiği her dosyada o tekrarı kesinleştirirdi. Bu yüzden şifreli altın dosyalar CBC modunda koşan aes128 ya da aes256 kullanır
Tanımlayıcı daha yumuşak bir takastır. ISO 32000-1 §14.4, ikinci /ID elemanının her değişiklikte değişmesini ister ve yeniden üretilebilir bir kayıt her iki yuvaya aynı değeri yazar. O değer nesne grafiğini hash'lediği için farklı içerik yine de farklı tanımlayıcılar alır. /ID'ye dayalı önbellek tutan bir okuyucu — bir ek açıklama deposu ya da form-veri yardımcı dosyası gibi — aynı içeriğin iki yeniden üretilebilir kaydını yine de tek dosya sayar. Bir test paketinde istediğiniz tam olarak budur
Yazıcının dışında kalan girdiler
ReproducibleOutput, yazıcının kendi başına soktuğu entropiyi kapsar. Birkaç girdi daha, derleme makinesinden ya da tanım gereği yeni bir an kaydeden işlemlerden gelir
- Saat dilimi soneki. Her PDF tarih dizisi yerel UTC ofsetiyle biter; aynı sabit tarih bir derleme ajanında
D:20260101000000+08'00', öbüründeD:20260101000000-05'00'okunur. Altın dosyalar tek makinedeki ya da aynı dilimi paylaşan makineler arasındaki çalıştırmalarda tutar. Dosyalar ofisler arasında geziyorsa ajanların saat dilimini sabitleyin - Artımlı güncellemeler.
SaveIncrementalUpdatekaynağın kalıcı tanımlayıcısını korur ve yeni bir değişiklik tanımlayıcısını öncekinden, hedef yoldan, tick sayısından ve geçerli zamandan kurar. Eklenen bir bölüm tanım gereği yeni bir değişikliktir. Eklenen deltaları değil, tam yeniden yazımları karşılaştırın - Doğrudan geçirme kısayolu.
SaveLoadedDocument, değişmemiş ve şifresiz bir kaynağı normalde yeniden serileştirmek yerine bayt bayt kopyalar. Bayrak altında dosya, sıralama ve tanımlayıcı kurallarının uygulanması için hep yeniden yazılır; bu da kaydı yavaşlatır ve girdinin asla bir kopyası olmamasını sağlar. Onu önceki bir yeniden üretilebilir kayıtla karşılaştırın, asla özgün girdiyle değil - Kendinizin eklediği zaman damgaları.
AddDocumentAttachment, gömülü dosyanın/Params/CreationDateile/ModDate'ini (ISO 32000-1 §7.11.4) çağrının anıyla damgalar; bir imza, imzalama zamanını/Miçine yazar. O tarihler çağıranın istediği olayları tanımlar; bayrak onlara dokunmaz. Ekleri ve imzaları bayt düzeyindeki durumların dışında tutun ya da karşılaştırmada o dizgileri maskeleyin - Kurulu yazı tipleri. Bir TrueType alt kümesi, testi koşan makinede kurulu yazı tipi dosyasından kesilir. Farklı Arial sürümlü iki ajan, aynı metin için farklı glif programları gömer ve alt küme öneki bunlarla değişebilir. Yazı tipi dosyalarını derleme imajıyla gönderin
Yüklü belgeler aynı kuralları izler. Özelliği yüklemeden önce kurun, düzenlemelerinizi yapın ve SaveLoadedDocument üzerinden kaydedin; çıktı, bir sonraki çalıştırma için başvuru olarak işleyebileceğiniz normalize edilmiş bir yeniden yazımdır
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.ReproducibleOutput := True; // yüklemeden önce ayarlanır
if Pdf.LoadFromFile('incoming.pdf') <= 0 then
raise Exception.Create('incoming.pdf did not load');
// ... test altındaki düzenlemeler ...
Pdf.SaveLoadedDocument('normalized.pdf'); // tam yeniden yazım, sıralı anahtarlar
finally
Pdf.Free;
end;
end;
Bayt-bayt aynı kayıtlar ayrıca titreyen bir uygunluk kontrolünü köşesine sıkıştırır. Yazıcı kararlılaştıktan sonra bir çalıştırmada geçip öbüründe düşen bir kontrol, testin kurduğu nesne grafiğine tepki veriyor demektir; örneğin tek bir doğrudan sözlüğün iki üsteye verilmesi gibi; doğrulayıcıdan önce test girdisini inceleyin. Katalogdan aşağı yapısal bir diff, iki çalıştırma arasında hangi nesnenin oynadığını gösterir
Yukarıda kullanılan ReproducibleOutput, DeterministicDictionaryOrder, VerifySavedOutput ve şifreleme özellikleri, Delphi ile C++Builder için birlikte gelen HotPDF Delphi Component'in parçasıdır