Teknik Makale

HotPDF'de Crypt Filter Politikaları: StmF, StrF ve EFF

HotPDF Delphi PDF component, ISO 32000-1 §7.6.5 crypt filter modelini tek bir anahtar yerine üç bağımsız politika olarak uygular: ConfigureCryptFilterDefaults, string filter /StrF, stream filter /StmF ve embedded-file filter /EFF değerlerini ayrı ayrı atar; SetStreamCryptFilter tek bir stream'i override eder; GetLoadedCryptFilterInfo ise gelen dosyanın ne bildirdiğini raporlar. Şifreli PDF interoperability hatalarının çoğu bu üçünün arasındaki boşluklarda yaşar

İnsanları bu katmana getiren hata şudur. Bir ekip, sayfa içeriği aşağı akıştaki araç tarafından okunabilir kalmalı ama ekli payload okunamamalı diye bir belge yayımlar; /EFF /StdCF ayarlar ve /StmF /Identity bırakır. Acrobat bunu iyi açar. Uyumlu bir üçüncü taraf reader, eki ciphertext garbage olarak verir; çünkü /EFF, embedded file'lara hangi filter'ın uygulanacağına dair üretici tarafı politikadır ve genel bir reader işaretlenmemiş stream'i yine /StmF üzerinden çözer. Düzeltme farklı bir /EFF değeri değildir. Düzeltme, embedded-file stream'in kendisine açık bir /Crypt filter koymaktır

Crypt filter katmanı gerçekte neyi kontrol eder?

Crypt filter'lar encryption algorithm ile object graph arasına oturur ve algoritmanın nasıl çalıştığını değil, hangi nesnelere dokunduğunu belirler. Encryption dictionary içindeki /CF sözlüğü adları filter tanımlarına eşler; her tanımda bir /CFM yöntemi, isteğe bağlı bir /Length ve bir /AuthEvent bulunur. Üç üst düzey giriş /StrF, /StmF ve /EFF daha sonra bu adlandırılmış filter'lardan hangisinin string'lere, açık filter taşımayan stream'lere ve embedded file'lara uygulanacağını seçer. HotPDF yerleşik handler'larının yazacağı şeyleri bilerek sınırlar. ConfigureCryptFilterDefaults, etkin handler için yalnızca ayrılmış adları kabul eder: Standard security handler /StdCF veya /Identity, public-key handler /DefaultCryptFilter veya /Identity yayımlar; başka her şey çağrı yerinde EArgumentException üretir. Harici üreticilerin başka adlarla yazdığı filter'lar load, inspection ve compatibility-rewrite yollarında yine korunur; yani HotPDF writer olarak muhafazakâr, reader olarak hoşgörülüdür. İki koruma daha vardır: belge serialization başladıktan sonra ve belge incremental update içindeyse çağrı EInvalidOpException üretir; çünkü aynı dosyanın revizyonları arasında encryption policy değişemez

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'wrapper.pdf';
    Pdf.OwnerPassword := 'owner-secret';
    Pdf.UserPassword := 'open-secret';
    Pdf.CryptKeyLength := aes128;
    // string'ler şifreli, sayfa stream'leri düz metin, ekler şifreli
    Pdf.ConfigureCryptFilterDefaults('StdCF', 'Identity', 'StdCF');
    Pdf.ActivateProtection := True;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(50, 50, 0, 'Visible stream operators');
    Pdf.AddDocumentAttachment('payload.bin', 'Encrypted payload');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Bir kısıtı baştan söylemek gerekir; çünkü geç kontrol edilir ve insanları şaşırtır. HotPDF'de adlandırılmış crypt filter'lar aes128, aes256 veya aesgcm belge şifrelemesini ister. RC4 k40 veya k128 üzerine bir filter policy kurarsanız encryption etkinleştirildiğinde çalışan validation pass, key type'ı sessizce yükseltmek yerine hata üretir. Bu, Delphi'deki AES-256 PDF encryption yolu ile aynı tasarım tavrıdır: çağıranın ne demek istediğini tahmin etmek yerine belirsiz configuration'ı reddet

/Length girişi neden iki farklı anlama gelir?

Çünkü specification, security handler'a göre onu iki farklı birimde tanımlar ve HotPDF ikisine de uymak zorundadır. /CFM değeri /V2 olan bir crypt filter dictionary'de /Length Standard security handler altında bayt, public-key handler altında bit cinsinden ifade edilir. /V ile yan yana duran encryption-dictionary /Length (ISO 32000-1 §7.6.2) ise her zaman bittir. /Length 16 taşıyan bir filter dictionary okuduğunuzda Standard-handler dosyasında 128 bitlik anahtar, public-key dosyasında reddedilen bir dosya elde edersiniz. HotPDF, loaded configuration'ı yakalarken bunu normalleştirir. /V2 filter /Length değerini yalnızca dosya public-key ile şifrelenmemişse sekizle çarpar, filter kendi değerini vermiyorsa document-level /Length değerine döner ve sonucu THPDFCryptFilterInfo.KeyLengthBits içinde saklar. AESV2 128 bite, AESV3 ve AESV4 256 bite sabitlenir; bu yöntemlerde pazarlık edilebilir key size yoktur. Katı kısım bundan sonra gelir: yalnızca 40 bit ve 128 bit /V2 kabul edilir. Başka bir uzunluğa çözülen filter unavailable olarak raporlanır ve çoğu üreticinin 128 kastettiği düşüncesiyle 128'e yuvarlanmak yerine işlem başarısız olur. Key length'i sessizce normalleştirmek, makinenizde çözülen ama başka hiçbir yerde çözülemeyen bir dosyayı böyle yayımlatır

var
  Reader: THotPDF;
  Info: THPDFCryptFilterInfo;
  I: Integer;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    if Reader.LoadFromFile('incoming.pdf', 'open-secret') <> 1 then
      Exit;
    // /StrF ve /StmF varsayılan olarak Identity; /EFF varsayılan olarak /StmF
    WriteLn(Reader.LoadedStringCryptFilterName);        // StdCF
    WriteLn(Reader.LoadedStreamCryptFilterName);        // Identity
    WriteLn(Reader.LoadedEmbeddedFileCryptFilterName);  // StdCF
    for I := 0 to Reader.GetLoadedCryptFilterCount - 1 do
      if Reader.GetLoadedCryptFilterInfo(I, Info) then
        if (Info.Method = hcfmV2) and
           not (Info.KeyLengthBits in [40, 128]) then
          raise Exception.CreateFmt(
            'crypt filter /%s: unsupported V2 key length %d',
            [String(Info.Name), Info.KeyLengthBits]);
  finally
    Reader.Free;
  end;
end;

/CFM /None neyi garanti eder ve /Identity nasıl farklıdır?

İkisi farklı yollarla aynı sonuca ulaşır ve onları birbirine karıştırmak lookup'ları bozar. /CFM değeri /None olan adlandırılmış bir filter ile /CFM girişini bütünüyle atlayan adlandırılmış bir filter, bu filter'ın encryption veya decryption yapmadığı anlamına gelir; HotPDF eksik girişi çözümlemeden önce None'a eşler, bu yüzden ikisi de kaydedilmiş key length sıfır ile hcfmNone olur. /Identity tür olarak farklıdır: /CF lookup'ını tamamen atlayan ayrılmış addır; belge /CF içinde hiç tanımlamadan /Identity'e başvurabilir. PDF name'leri büyük-küçük harfe duyarlıdır; bu da başka bir uygulama ayrıntısını pazarlık konusu olmaktan çıkarır: hiçbir crypt filter lookup'ı case-insensitive olamaz. HotPDF, /CF alt sözlüğü adlarını, filter /Length girişini ve stream /Type kontrolünü case-sensitive dictionary lookup'larıyla çözer. /stdcf tanımlayıp /StmF'yi /StdCF'ye yönlendiren bir dosya bozuktur; ikisini aynı anahtar saymak, tespit edilebilir bir authoring hatasını belge içindeki her stream'e sessizce yanlış anahtar uygulayan bir sonuca çevirir

/EFF'i embedded-file stream'lerinde kalıcı kılmak

/EFF ile /StmF farklı olduğunda embedded-file stream, /Filter dizisinde açık bir baştaki /Crypt girişi ve aynı array konumunda /Name taşıyan eşleşen bir /DecodeParms dictionary ister. HotPDF bunu save zamanında stream başına çözer: /Type /EmbeddedFile tespit eder, yapılandırılmış embedded-file filter'ı devralır ve yalnızca bu devralınan ad etkin stream default'undan farklı olduğunda açık /Crypt işaretini yayımlar. /EFF ile /StmF aynıysa işaret yazılmaz, çünkü reader zaten aynı filter'ı çözer. Ardından array konumu ad kadar önemlidir. HotPDF bir stream'i yeniden okurken /Filter içinde /Crypt girişini tarar, index'ini kaydeder ve /Name değerini bulmak için /DecodeParms dizisinde aynı index'e bakar. Index 0'daki /Crypt'i index 1'deki parametrelerle eşlerseniz sonuç sizin filter'ınız değil /Identity olur. Stream daha önce /Filter taşıyıp /DecodeParms taşımıyorsa writer'ın parameter array'i null ile doldurmasının nedeni de budur: konumlar hizalı kalmalıdır

Altta daha keskin bir tuzak vardır. Mevcut /Filter veya /DecodeParms dolaylı bir nesneyse — birden çok stream arasında tek filter array paylaşan generator'ların dosyalarında bu yaygındır — yerinde /Crypt eklemek paylaşılan filter graph'ı değiştirir ve ona işaret eden diğer her stream'i bozar. HotPDF dolaylı nesneyi çözer ve önce stream'e özel doğrudan bir nesneye klonlar; özgün indirect root'un yeni array içine gömülmemesi için object ve generation number'larını temizler. Daha önce ASCIIHexDecode kullanan bir stream için serialize edilmiş sonuç /Filter [ /Crypt /ASCIIHexDecode ] ve /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ] olur. Aynı konumsal disiplin, yüklenmiş bir PDF'den görüntüleri decode filter'ları üzerinden çıkarırken gezdiğiniz diğer her filter zincirine de uygulanır

// Editor yüklü bir belgeyi zaten tutuyor ve ContentStream'in
// /Filter değeri dolaylı bir /ASCIIHexDecode adı
Editor.OwnerPassword := 'owner-secret';
Editor.UserPassword := 'open-secret';
Editor.CryptKeyLength := aes128;
Editor.ConfigureCryptFilterDefaults('StdCF', 'Identity');
Editor.SetStreamCryptFilter(ContentStream, 'StdCF');
Editor.ActivateProtection := True;
Editor.SaveLoadedDocument('out.pdf');

// Boş ad override'ı temizler ve sonraki save'de eski /Crypt
// girdisini decode parametreleriyle birlikte kaldırır
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');

Object stream'ler document /Encrypt politikasını devralır mı?

Hayır; öyle varsaymak garbage üretmenin güvenilir bir yoludur. Bir object stream, gerçek /StmF politikasını veya kendi açık /Crypt işaretini izlemelidir: yalnızca bir /Encrypt dictionary'sinin bulunması her /ObjStm container'ını ciphertext yapmaz. /StmF /Identity olan bir belgede string'ler tamamen şifreli olsa da object stream'ler düz metindir; onları yine decrypt eden bir decoder, inflate aşamasına hiçbir zaman deflate çıktısı olmamış girdiler verir

Üye nesneler için iki kez okunması gereken sonuç şudur. ISO 32000-1 §7.5.7'ye göre şifreli bir object stream içindeki string'ler container'ın kendisi decrypt edildiğinde zaten düz metindir; onları yeniden decrypt etmek double-decrypt olur. HotPDF, her type-2 nesnesinin container'ının şifreli olup olmadığını sorarak bunu korur ve şifreliyse nesneyi atlar; atlamaları XRefProbeDecryptObjStmSkips içine guard'ın çalıştığına dair doğrudan kanıt olarak sayar. Container düz metin olduğunda üye string'ler hiçbir şey tarafından kapsanmamıştır; bu nedenle HotPDF bu üyeleri materialize eder ve /StrF'yi her birine ayrı uygular — uygulamanın gerçekte yaptığı gibi containing /ObjStm object number'ıyla değil, member object number ve generation ile anahtarlayarak. Mixed-policy bir dosyada bunun tersini yaparsanız her sıkıştırılmış nesnedeki her string noise olarak decode edilir. Bunun etrafındaki container-level kurallar PDF object stream'leri ve incremental update'ler notlarında daha ayrıntılı ele alınır

HotPDF'in tahmin etmeyi reddettiği yer

Crypt filter semantiği /V 4'ün altında yoktur; bu nedenle HotPDF böyle bir dosyada stream başına override'ı, conforming hiçbir reader'ın uymayacağı bir /Crypt işareti yazmak yerine açık bir hatayla reddeder. Okuma tarafında da aynı şey geçerlidir: /V değeri 4'ün altında olan bir encryption dictionary, bildirilecek bir şey olmadığı için üç loaded filter adının tamamını temizler. Üç sınır daha bilerek zorlanır:

  • Public-key ile şifrelenmiş bir belgede Identity olmayan stream başına filter reddedilir; çünkü public-key handler altında stream'e özel bir policy, HotPDF'in henüz üretmediği stream'e özel recipient envelope ister
  • /EFF etkin /StmF'den farklı olan public-key şifreli embedded file'lar aynı nedenle reddedilir; kimsenin decrypt edemeyeceği bir biçimde yazılmaları tercih edilmez
  • AES-256 direct-file fast path yalnızca string'ler, stream'ler ve embedded file'lar aynı crypt filter method'a çözümleniyor ve dosyadaki hiçbir nesne açık /Crypt taşımıyorsa uygulanır; mixed policy veya düz metin metadata, full object-graph yoluna fallback'i zorlar

Bunların hiçbiri performance kararı değildir. Yanlış tahminin bir viewer'da açılan, diğerinde başarısız olan ve ancak müşteri bildirdiğinde geliştiriciye sinyal veren bir PDF ürettiği yerleri işaretlerler. ConfigureCryptFilterDefaults veya save zamanında bir refusal bir exception'a mal olur; sessizce yanlış anahtarlanmış bir embedded file ise bir support döngüsüne mal olur. Seçici olarak düz metin sayfa içeriği ve şifreli ekler üreten ya da tüketen, PDF 2.0 şifreli payload wrapper'larıyla veya seçmediğiniz crypt filter politikalarına sahip dosyalarla interoperability yapan Delphi veya C++Builder yazılımı kuruyorsanız, burada anlatılan crypt filter API, encryption, object stream ve incremental update yollarının yanında güncel HotPDF Delphi PDF component içinde sunulur