Teknik Makale

Delphi'de PDFlibPas ile SASLprep AES-256 ASCII Dışı Şifre

ASCII dışı bir şifreyle şifrelenmiş bir AES-256 PDF, onu yazan programda açılır ve başka hiçbir yerde açılmaz. Neden neredeyse her zaman eksik bir hazırlık adımıdır: ISO 32000-2 §7.6.4.3.3, şifrenin UTF-8 kodlanıp hash'lenmeden önce stringprep'in SASLprep profiliyle işlenmesini gerektirir. Delphi ve C++Builder için PDF kütüphanesi olan PDFlibPas, bu hazırlığı Encrypt, EncryptFile ve DecryptFile içinde gerçekleştirir

Bu, yanlış şifre hikayesi değil ve izin biti hikayesi de değil. Kullanıcılarınız sizin hiç vermediğiniz bir şifre yazıyorsa, şifreli PDF şifrelerini yeniden deneme üzerine makaledeki yeniden deneme makinesi istediğinizdir, ve mevcut bir dosyanın gerçekte neyi zorladığını çözmeye çalışıyorsanız, şifreleme ve izinler denetimi o zemini kapsar. Bu makale ise daha dar ve daha tuhaf bir konu: şifre doğrudur, kullanıcı onu doğru yazmıştır ve dosya yine de başka bir yerde açılmayı reddeder

ASCII dışı bir şifre neden bir okuyucuda açılır da diğerinde açılmaz?

Çünkü iki program, aynı tuş vuruşlarından farklı byte dizilerini hash'ler. ISO 32000-2 §7.6.4.3.3'teki revizyon 6 anahtar türetmesi, şifreyi UTF-8 byte'lar olarak alır, 127 bayta kırpar, bir tuz ekler ve sertleştirilmiş hash'i çalıştırır; sonuç, şifreleme sözlüğündeki /U ve /O girdilerine karşı kontrol edilir. Bu zincirde bulanık hiçbir şey yoktur. Girdideki herhangi bir yerdeki tek bir farklı byte tamamen farklı bir özet üretir, doğrulama başarısız olur ve okuyucunun söyleyebileceği tam olarak tek bir şey vardır: yanlış şifre

Baytlar farklılaşır çünkü Unicode, aynı görünen bir şifreyi yazmanın birkaç yolunu sunar. Bir Çince şifre bir giriş yönteminden önceden birleştirilmiş karakterler olarak, diğerinden ise uyumluluk formları olarak gelebilir. Bir kelime işlemciden kopyalanan bir Almanca veya Fransızca şifre, kullanıcının sıradan bir boşluk olduğuna inandığı yerde bir SIFIR GENİŞLİKTE BOŞLUK OLMAYAN BOŞLUK (U+00A0) veya hiçbir şey olarak görüntülenen bir YUMUŞAK TİRE (U+00AD) taşıyabilir. SASLprep, herhangi biri herhangi bir şeyi hash'lemeden önce tüm bunları tek bir kanonik forma daraltmak için vardır, böylece her uyumlu uygulama aynı niyetten aynı anahtarı türetir

SASLprep bir şifre hakkında gerçekte neyi değiştirir?

RFC 4013, SASLprep'i RFC 3454'teki stringprep çerçevesinin bir profili olarak tanımlar ve bu tek bir dönüşüm değil, dört sıralı adımdır. Önce eşleme gelir: RFC 3454 tablo C.1.2 (ASCII olmayan boşluklar) U+0020'ye eşlenir ve tablo B.1 (genellikle hiçbir şeye eşlenen karakterler) tamamen silinir. Ardından Unicode NFKC'ye normalleştirme gelir, bu da uyumluluk karakterlerini ve birleştirme dizilerini katlayan adımdır. Sonra yasak-çıktı kontrolü, tablolar C.2.1'den C.9'a kadar olan her şeyi reddeder. Son olarak, normalleştirilmiş dizeye RFC 3454 bölüm 6'daki çift yönlü kural uygulanır

PDFlibPas, tüm profili tek bir giriş noktası sunan PDFlibSASLprep biriminde uygular. PLSASLprepPassword, ham şifreyi alır, hazırlanmış formu bir var parametresine yazar ve şifrenin reddedilmesi gerektiğinde False döndürür. Fonksiyon, mutlu yolda kasıtlı olarak toplamdır: yalnızca ASCII'den oluşan bir şifre byte özdeş olarak geri döner, dolayısıyla mevcut dağıtımlar hakkında hiçbir şey değişmez

uses
  PDFlibSASLprep;

var
  Prepared: WideString;
begin
  // RFC 4013: mapping, then NFKC, then prohibited output, then the bidi rule
  PLSASLprepPassword('I' + WideChar($00AD) + 'X', Prepared);  // -> 'IX'   B.1 deletes SOFT HYPHEN
  PLSASLprepPassword('a' + WideChar($00A0) + 'b', Prepared);  // -> 'a b'  C.1.2 maps NBSP to U+0020
  PLSASLprepPassword(WideString(WideChar($00AA)), Prepared);  // -> 'a'    NFKC folds ORDINAL INDICATOR
  PLSASLprepPassword(WideString(WideChar($2168)), Prepared);  // -> 'IX'   NFKC folds ROMAN NUMERAL NINE
  PLSASLprepPassword('user', Prepared);                       // -> 'user' ASCII is never touched
end;

Tabloların çözmediği U+200B belirsizliği

Bir kod noktası aynı anda iki RFC 3454 tablosuna düşer ve iki tablo birbiriyle çelişir. SIFIR GENİŞLİKTE BOŞLUK (U+200B), kuralın onu U+0020'ye eşlemeyi söylediği C.1.2 aralığı U+2000 ila U+200B içine düşer ve aynı zamanda kuralın onu silmeyi söylediği B.1 aralığı U+200B ila U+200D içine de düşer. Eşleme adımını herhangi bir sırada okuyun, aynı şifreden farklı baytlar elde edersiniz: a+U+200B+b, C.1.2 altında a b'ye, B.1 altında ise ab'ye hazırlanır. RFC 4013 her iki tabloyu da adlandırır ve hangisinin kazandığını söylemez, dolayısıyla bu bir okuma hatası değil, spesifikasyonda gerçek bir belirsizliktir. PDFlibPas önce C.1.2 üyeliğini test eder ve bu yüzden U+200B'yi bir boşluğa eşler, ki bu diğer yaygın olarak dağıtılan stringprep uygulamalarının yerleştiği davranıştır; onlarla eşleşmek burada önemli olan tek şeydir, çünkü amaç müşterinin kullandığı hangi okuyucu olursa olsun onunla byte anlaşmasıdır

Eski dosyaları okumak: önce hazırlanmış, sonra ham

Bu düzeltme kendi uyumluluk sorununu yaratır. Değişiklikten önce yazılmış her AES-256 dosyası ham UTF-8 şifresini hash'lemiştir, dolayısıyla okuyucuyu kesinlikle uyumlu yapmak müşterileri kendi arşivlerinden dışlar. PDFlibPas bunu okuma tarafında iki adayı sırayla deneyerek çözer. TPDFDocument.SetPassword, hazırlanmış formla başlayan ve ham forma geri dönen bir aday listesi oluşturur ve hazırlanmış girdiyi yalnızca belge gerçekten AES-256 ise ve iki form farklıysa ekler. ASCII bir şifre için formlar aynıdır, liste bir girdi tutar ve tüm mekanizmanın maliyeti tek bir dize karşılaştırmasıdır. DecryptFile, doğrudan AES-256 yeniden yazma yolu boyunca aynı şeyi yapar, önce hazırlanmış şifreyle PLDirectDecryptFileAES256'ı çağırır

var
  Lib: TPDFlib;
  Bytes: AnsiString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.DrawText(100, 100, 'saslprep roundtrip');
    // Strength 3 and 4 are the two AES-256 values; both are prepared before hashing
    Lib.Encrypt('ow' + WideChar($00AD) + 'ner', 'pa' + WideChar($00AD) + 'ss', 4,
      Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1));
    Bytes := Lib.SaveToString;
  finally
    Lib.Free;
  end;

  Lib := TPDFlib.Create;
  try
    // 'pass' is what SASLprep produced and what any conforming reader computes,
    // so the plain ASCII form opens a file created with the soft-hyphen form
    if Lib.LoadFromString(Bytes, 'pass') = 1 then
      Caption := IntToStr(Lib.PageCount);
  finally
    Lib.Free;
  end;
end;

Geri dönüş, kopyalamaya değer bir koruma taşır. DecryptFile'daki ikinci deneme yalnızca hazırlanmış ve ham formlar farklıysa ve ilk deneme hiçbir sert hata kodu bildirmediyse çalışır. Yapısal bir başarısızlık, girdinin bozuk olduğu veya varsaydığınız şifreleme revizyonu olmadığı anlamına gelir ve bozuk bir dosyayı başka bir şifreyle yeniden denemek, düşman girdi üzerinde ikinci bir tam ayrıştırma boşa harcamaktır; bu içgüdünün arkasındaki mantık güvenilmeyen PDF'leri güvenli ayrıştırma üzerine notta anlatılmıştır. Ayrıca yazma tarafında hiçbir geri dönüşün olmadığını ve bu asimetrinin kasıtlı olduğunu unutmayın. Okuma geçmişe tolerans gösterir, yazma göstermez: her yeni AES-256 dosyası uyumlu baytları alır

Hangi şifreler doğrudan reddedilir ve hata 604 nedir?

SASLprep bir şifreyi tamamen reddedebilir ve bunu yaptığında, şifreleme sessizce bir şey değiştirmek yerine gürültülü bir şekilde başarısız olmalıdır. Encrypt ve EncryptFile, Strength 3 veya 4 olduğunda hem sahip hem de kullanıcı şifrelerini hazırlar, reddedilmede 0 döndürür ve LastErrorCode'u 604 olan PDFLIB_ERROR_PASSWORD_SASLPREP'e ayarlar. İki girdi ailesi bunu tetikler. Yasak-çıktı tabloları kontrol karakterlerini (C.2.1 ve C.2.2), özel kullanım kod noktalarını (C.3), karakter olmayanları (C.4), yalnız vekilleri (C.5), U+FFFD'yi (C.6), ideografik açıklama karakterlerini (C.7) ve görüntü kontrolü ile etiketleme aralıklarını (C.8 ve C.9) reddeder. Ayrıca, RFC 3454 bölüm 6 çift yönlü kuralı, tablo D.1'den bir RandALCat karakteri içeren herhangi bir dizeyi, dize hem böyle bir karakterle başlamıyor hem bitmiyorsa veya hiç soldan sağa harf içermiyorsa reddeder

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // U+0007 is a C.2.1 control character, so preparation refuses the password
    if Lib.Encrypt('owner', 'bad' + WideChar($0007), 4,
         Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1)) = 0 then
    begin
      if Lib.LastErrorCode = PDFLIB_ERROR_PASSWORD_SASLPREP then  // 604
        ShowMessage('The password contains characters that PDF encryption does not permit.');
    end;
  finally
    Lib.Free;
  end;
end;

Destek masanızı şaşırtacak olan o çift yönlü kuraldır. Batı rakamıyla biten bir Arapça veya İbranice şifre, ya da ortasında başıboş bir Latin harfi olan bir şifre, giriş alanında son derece makul görünse bile spesifikasyon tarafından reddedilir. 604'ü genel bir şifreleme hatası olarak değil, şifre karakterleri hakkında bir mesaj olarak yüzeye çıkarın, yoksa biri anahtar türetmenizde bir hata aramak için bir öğleden sonra harcayacaktır

Dürüst sınırlar: NFKC, yaklaşık bir LCat ve bir Delphi tuzağı

Uygulamanın iki bölümü yaklaşımdır ve ikisi de gömülmek yerine açıkça belirtilmeyi hak eder. NFKC normalleştirmesi, Normaliz.dll'den dinamik olarak yüklenen Windows NormalizeString API'si tarafından gerçekleştirilir. Bu kütüphane kullanılamadığında, eşlenmiş dize normalleştirilmeden kullanılır, bu da eşleme ve yasaklama adımlarının hâlâ çalıştığı ama uyumluluk katlamasının çalışmadığı anlamına gelir. Pratikte DLL, Vista'dan bu yana her Windows sürümüyle birlikte gönderildi, dolayısıyla bozulmuş yol canlı bir endişe olmaktan çok Vista öncesi ve Windows dışı bir endişedir, ama NFKC katlamasına dayanan bir şifre orada farklı baytlar üretir ve bu gerçek, uzak da olsa, bir sapmadır. Çift yönlü kontrol ikinci yaklaşımdır: LCat karakterlerini tespit etmek, tam RFC 3454 tablo D.2 yerine yaygın harf aralıklarını kullanır ve bu hatanın yönü onu kabul edilebilir kılan şeydir. Kaçırılan bir LCat karakteri, yalnızca çift yönlü kuralın spesifikasyonun reddedeceği yerde geçmesine neden olabilir, hiçbir zaman tersine, ve hiçbir zaman eşleme veya normalleştirme adımlarına dokunmaz, dolayısıyla kabul edilen bir şifrenin hazırlanmış byte dizisi değişmez. Kalan risk bu yüzden bir byte sapması değil, bir politika sapmasıdır: daha katı bir uygulamanın kabul etmeyi tamamen reddedeceği egzotik bir yazıya sahip şifre. Her iki tarafın da kabul ettiği her şifre aynı şekilde hash'lenir, ki birlikte çalışabilirliğin gerçekte bağlı olduğu özellik budur

Son olarak, daha önce karşılaşmadıysanız bir saat kaybettiren bir Delphi sözdizimi tuzağı. Bir fonksiyon bir prosedürel tip döndürdüğünde, parantez olmadan atamak onu çağırmaz. Derleyici Proc := GetNormalizeProc;'i GetNormalizeProc'un kendisinin adresini almak olarak okur, sonra çağırma kurallarının farklı olduğuna dair yardımcı olmayan bir şikayetle E2009 bildirir, çünkü erişimci varsayılan kuralı kullanırken içe aktarılan API tipi stdcall'dır. Boş parantezler zorunludur

type
  TNormalizeString = function(NormForm: Integer; SrcString: PWideChar; SrcLength: Integer;
    DstString: PWideChar; DstLength: Integer): Integer; stdcall;

function GetNormalizeProc: TNormalizeString;   // loads Normaliz.dll on first use
...
var
  Proc: TNormalizeString;
begin
  // Proc := GetNormalizeProc;   // E2009: reads as @GetNormalizeProc, conventions differ
  Proc := GetNormalizeProc();    // correct: calls the accessor and assigns its result
  if not Assigned(Proc) then
    Exit;                        // no NFKC available, mapped string is used as-is
end;

Şifre hazırlığı, hiçbir özellik listesinde görünmeyen ama şifreli bir belgenin başka bir yerelde bir müşteriyle temasa hayatta kalıp kalmayacağına karar veren o ayrıntılardan biridir. Burada anlatılan Encrypt, EncryptFile, DecryptFile ve SetPassword giriş noktaları, ürün sayfası tam şifreleme referansını ve eksiksiz hata kodu tablosunu taşıyan, Delphi ve C++Builder için losLab PDF Developer Library Pascal Edition'ın bir parçasıdır