Artikel Teknis

Crypt Filter PDF di Delphi: Kebijakan StmF, StrF, dan EFF

HotPDF Delphi PDF component mengimplementasikan model crypt filter ISO 32000-1 §7.6.5 sebagai tiga policy independen, bukan satu switch: ConfigureCryptFilterDefaults menetapkan string filter /StrF, stream filter /StmF, dan embedded-file filter /EFF secara terpisah, SetStreamCryptFilter meng-override satu stream, dan GetLoadedCryptFilterInfo melaporkan deklarasi file yang masuk. Sebagian besar bug interoperabilitas encrypted PDF hidup di celah antara ketiganya

Inilah kegagalan yang membuat orang masuk ke layer ini. Sebuah tim mengirim dokumen yang page content-nya harus tetap terbaca oleh tool downstream, tetapi payload terlampir tidak boleh terbaca, sehingga mereka menetapkan /EFF /StdCF dan membiarkan /StmF /Identity. Acrobat membukanya dengan baik. Reader pihak ketiga yang conforming mengembalikan attachment sebagai ciphertext garbage, karena /EFF adalah policy sisi producer tentang filter untuk embedded file dan general reader tetap menyelesaikan stream tanpa penanda melalui /StmF. Perbaikannya bukan nilai /EFF yang berbeda. Perbaikannya adalah explicit /Crypt filter pada embedded-file stream itu sendiri

Apa yang sebenarnya dikendalikan crypt filter layer?

Crypt filter berada di antara encryption algorithm dan object graph, dan menentukan object mana yang disentuh algorithm, bukan cara algorithm bekerja. Dictionary /CF di dalam encryption dictionary memetakan nama ke filter definition, yang masing-masing membawa method /CFM, /Length opsional, dan /AuthEvent. Tiga entry tingkat atas /StrF, /StmF, dan /EFF kemudian memilih filter bernama yang berlaku untuk string, stream tanpa filter eksplisit, dan embedded file. HotPDF sengaja membatasi apa yang dapat ditulis oleh built-in handler-nya. ConfigureCryptFilterDefaults hanya menerima nama reserved untuk handler aktif: Standard security handler mengeluarkan /StdCF atau /Identity, public-key handler mengeluarkan /DefaultCryptFilter atau /Identity, dan apa pun selain itu mengangkat EArgumentException di call site. Filter yang ditulis producer eksternal dengan nama lain tetap dipertahankan pada jalur load, inspection, dan compatibility-rewrite, sehingga HotPDF konservatif sebagai writer tetapi permisif sebagai reader. Dua guard tambahan berlaku: pemanggilan mengangkat EInvalidOpException setelah document serialization dimulai, dan kembali mengangkatnya jika dokumen berada dalam incremental update, karena encryption policy tidak dapat diubah di antara revision file yang sama

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 terenkripsi, page stream plaintext, attachment terenkripsi
    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;

Satu constraint perlu dinyatakan di awal karena baru diperiksa belakangan dan sering mengejutkan. Named crypt filter di HotPDF membutuhkan document encryption aes128, aes256, atau aesgcm. Konfigurasikan filter policy di atas RC4 k40 atau k128, maka validation pass yang berjalan ketika encryption diaktifkan akan mengangkat error, bukan diam-diam menaikkan key type. Ini adalah sikap desain yang sama seperti pada jalur AES-256 PDF encryption di Delphi: tolak konfigurasi ambigu, jangan menebak maksud caller

Mengapa entry /Length berarti dua hal yang berbeda?

Karena spesifikasi mendefinisikannya dalam dua unit berbeda bergantung pada security handler, dan HotPDF harus menghormati keduanya. Dalam crypt filter dictionary dengan /CFM berupa /V2, entry /Length dinyatakan dalam byte pada Standard security handler dan dalam bit pada public-key handler. /Length pada encryption dictionary yang berada di samping /V (ISO 32000-1 §7.6.2) selalu dalam bit. Baca filter dictionary yang membawa /Length 16, dan Anda memiliki key 128-bit pada file Standard-handler serta file yang ditolak pada public-key. HotPDF menormalisasi hal ini ketika menangkap loaded configuration. HotPDF mengalikan /Length filter /V2 dengan delapan hanya ketika file bukan public-key encrypted, menggunakan /Length tingkat dokumen jika filter tidak memiliki entry sendiri, lalu menyimpan hasilnya dalam THPDFCryptFilterInfo.KeyLengthBits. AESV2 dipatok pada 128 bit dan AESV3 serta AESV4 pada 256 bit, karena method tersebut tidak memiliki ukuran key yang dapat dinegosiasikan. Bagian ketatnya muncul setelah itu: hanya /V2 40-bit dan 128-bit yang diterima. Filter yang ter-resolve ke panjang lain dilaporkan sebagai unavailable dan operasi gagal, bukan dibulatkan ke 128 dengan anggapan bahwa sebagian besar producer sebenarnya bermaksud 128. Menormalkan key length secara diam-diam adalah cara mengirim file yang dapat didekripsi di mesin Anda tetapi tidak di tempat lain

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 dan /StmF default ke Identity; /EFF default ke /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;

Apa yang dijamin /CFM /None, dan apa bedanya dengan /Identity?

Keduanya mencapai hasil yang sama melalui jalur berbeda, dan mencampuradukkan keduanya merusak lookup. Named filter dengan /CFM berupa /None, serta named filter yang sama sekali tidak memiliki /CFM, sama-sama berarti filter ini tidak melakukan encryption atau decryption — HotPDF memetakan entry yang hilang ke None sebelum resolving, sehingga keduanya berakhir pada hcfmNone dengan recorded key length nol. /Identity berbeda secara mendasar: ini adalah nama reserved yang melewati lookup /CF sepenuhnya, sehingga dokumen dapat mereferensikan /Identity tanpa mendefinisikannya di dalam /CF. PDF name bersifat case-sensitive, dan itu membuat satu detail implementasi tidak dapat ditawar: lookup crypt filter tidak boleh case-insensitive. HotPDF menyelesaikan nama sub-dictionary /CF, entry filter /Length, dan pemeriksaan stream /Type melalui dictionary lookup yang case-sensitive. File yang mendefinisikan /stdcf sementara /StmF menunjuk /StdCF adalah malformed, dan memperlakukan keduanya sebagai key yang sama akan mengubah bug authoring yang dapat dideteksi menjadi key yang salah yang diterapkan diam-diam pada setiap stream dalam dokumen

Membuat /EFF benar-benar berlaku pada embedded-file stream

Ketika /EFF berbeda dari /StmF, embedded-file stream membutuhkan entry /Crypt eksplisit di awal /Filter serta dictionary /DecodeParms yang cocok dan membawa /Name pada posisi array yang sama. HotPDF menghitungnya per stream saat save: mendeteksi /Type /EmbeddedFile, mewarisi embedded-file filter yang dikonfigurasi, lalu mengeluarkan marker /Crypt eksplisit hanya ketika nama hasil pewarisan berbeda dari stream default efektif. Ketika /EFF dan /StmF sama, tidak ada marker yang ditulis karena reader akan menyelesaikan filter yang sama. Posisi array sama pentingnya dengan nama. Saat HotPDF membaca kembali stream, ia memindai /Filter untuk entry /Crypt, mencatat index-nya, lalu mencari index yang sama dalam array /DecodeParms untuk menemukan /Name. /Crypt pada index 0 yang dipasangkan dengan parameter pada index 1 akan diselesaikan menjadi /Identity, bukan filter Anda. Itu juga alasan writer mengisi array parameter dengan null ketika stream sebelumnya memiliki /Filter tetapi tidak memiliki /DecodeParms: posisi harus tetap sejajar

Ada jebakan yang lebih tajam di baliknya. Jika /Filter atau /DecodeParms yang ada merupakan indirect object — lazim pada file dari generator yang berbagi satu filter array di banyak stream — menyisipkan /Crypt di tempatnya akan memutasi shared filter graph dan merusak setiap stream lain yang menunjuk padanya. HotPDF me-resolve indirect object tersebut lalu meng-clone-nya menjadi direct object privat-stream terlebih dahulu, dengan menghapus object number dan generation number agar indirect root asli tidak pernah tertanam di dalam array baru. Untuk stream yang sudah menggunakan ASCIIHexDecode, hasil serialisasinya adalah /Filter [ /Crypt /ASCIIHexDecode ] dengan /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Disiplin posisi yang sama mengatur setiap filter chain lain, termasuk yang Anda telusuri ketika mengekstrak image dari PDF loaded melalui decode filter-nya

// Editor sudah memegang dokumen loaded, dan ContentStream adalah
// THPDFStreamObject yang /Filter-nya merupakan /ASCIIHexDecode indirect
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');

// Nama kosong menghapus override dan membuang /Crypt yang stale
// bersama decode parameter-nya pada save berikutnya
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');

Apakah object stream mewarisi policy /Encrypt dokumen?

Tidak, dan menganggap demikian adalah cara yang andal untuk menghasilkan garbage. Object stream harus mengikuti policy /StmF aktual atau marker /Crypt eksplisit miliknya sendiri: keberadaan dictionary /Encrypt saja tidak membuat setiap container /ObjStm menjadi ciphertext. Dokumen dengan /StmF /Identity memiliki object stream plaintext meskipun string-nya terenkripsi penuh, dan decoder yang tetap mendekripsinya akan memberikan input yang tidak pernah merupakan output deflate kepada tahap inflate

Konsekuensi untuk member object adalah bagian yang perlu dibaca dua kali. Menurut ISO 32000-1 §7.5.7, string di dalam object stream yang terenkripsi sudah plaintext setelah container-nya sendiri didekripsi, sehingga mendekripsinya lagi akan menjadi double-decrypt. HotPDF menjaganya dengan memeriksa apakah container setiap type-2 object terenkripsi dan melewati object tersebut jika memang demikian, lalu menghitung skip ke dalam XRefProbeDecryptObjStmSkips sebagai bukti langsung bahwa guard aktif. Saat container berupa plaintext, member string tidak pernah tercakup oleh apa pun, sehingga HotPDF mematerialisasikan member tersebut dan menerapkan /StrF pada masing-masing secara individual — dikunci, sebagaimana implementasinya, dengan member object number dan generation, bukan dengan object number /ObjStm yang memuatnya. Balikkan aturan ini pada file mixed-policy, dan setiap string di setiap compressed object akan ter-decode menjadi noise. Aturan tingkat container untuk kasus ini dibahas lebih lanjut dalam catatan tentang PDF object stream dan incremental update

Di mana HotPDF menolak untuk menebak?

Semantik crypt filter tidak ada di bawah /V 4, sehingga HotPDF menolak per-stream override pada file semacam itu dengan error eksplisit, bukan menulis marker /Crypt yang tidak akan dihormati reader conforming mana pun. Hal yang sama berlaku di sisi read: encryption dictionary dengan /V di bawah 4 mengosongkan ketiga nama filter yang dimuat, karena tidak ada apa pun yang dapat dilaporkan. Tiga batas tambahan juga ditegakkan dengan sengaja:

  • Per-stream filter non-Identity pada public-key encrypted document ditolak, karena policy khusus stream di bawah public-key handler membutuhkan stream-specific recipient envelope yang belum dikeluarkan HotPDF
  • Embedded file terenkripsi public-key yang /EFF-nya berbeda dari /StmF efektif ditolak karena alasan yang sama, bukan ditulis dalam bentuk yang tidak dapat didekripsi siapa pun
  • AES-256 direct-file fast path hanya berlaku ketika string, stream, dan embedded file semuanya ter-resolve ke crypt filter method yang sama dan tidak ada object dalam file yang membawa /Crypt eksplisit; mixed policy atau plaintext metadata memaksa fallback ke full object-graph path

Semua batas ini bukan keputusan performa. Batas tersebut menandai tempat di mana tebakan yang salah menghasilkan PDF yang terbuka di satu viewer, gagal di viewer lain, dan tidak memberi sinyal apa pun kepada developer sampai customer melaporkannya. Penolakan di ConfigureCryptFilterDefaults atau pada waktu save hanya memerlukan satu exception; embedded file dengan key yang salah secara diam-diam dapat menghabiskan satu siklus support. Jika Anda membangun software Delphi atau C++Builder yang menghasilkan atau mengonsumsi encrypted PDF — page content yang sengaja plaintext dengan attachment terenkripsi, payload wrapper terenkripsi PDF 2.0, atau interoperabilitas dengan file yang crypt filter policy-nya tidak Anda pilih — API crypt filter yang dijelaskan di sini tersedia di HotPDF Delphi PDF component saat ini, bersama jalur encryption, object stream, dan incremental update yang menjadi fondasinya