Artikel Teknis

Anda Tidak Bisa Menambal PDF Terenkripsi secara Diam-diam di Delphi

Ambil sebuah PDF invoice yang sudah membawa enkripsi AES-256 dan minta PDFium Component untuk Delphi dan C++Builder (PDFiumPas) mencapnya PDF/A untuk retensi arsip, atau menandatanganinya dengan PAdES, lewat sebuah incremental update alih-alih penulisan ulang penuh. Library ini tidak akan sampai ke sana dengan menambal byte terenkripsi secara langsung: keenam injector marker kesesuaiannya mendeteksi sebuah entry /Encrypt yang sudah ada dan meneruskan sumbernya byte-demi-byte tanpa berubah, dan penandatangan PAdES-nya memunculkan sebuah exception alih-alih memancarkan sebuah signature yang tidak akan diterima validator mana pun

Itu adalah pertanyaan yang berbeda dari mengaudit sebuah PDF yang tidak Anda buat untuk risiko tersembunyi, yang merupakan latihan read-only tersendiri. Artikel ini tentang sisi tulis dari batas kepercayaan yang sama: apa yang diizinkan dilakukan kode Anda sendiri terhadap sebuah file yang byte-nya sudah terkunci di balik password milik orang lain, pada saat kode itu mencoba menambahkan apa pun padanya setelah fakta

Apa yang diwajibkan ISO 32000-1 ketika Anda memperbarui sebuah PDF terenkripsi?

ISO 32000-1 §7.5.6 mewajibkan trailer sebuah incremental update untuk mengulang setiap entry dari trailer sebelumnya kecuali /Prev, dan Table 15 mendaftarkan /Encrypt di antara entry yang bisa dibawa sebuah trailer. Hilangkan itu dari trailer baru dan sebuah reader yang sesuai standar tidak punya alasan untuk meragukan penghilangan itu: trailer terbaru berwenang, sehingga sebuah reader yang tidak menemukan /Encrypt di sana memutuskan seluruh file tidak terenkripsi dan mencoba mem-parse tubuh yang lebih lama dan masih tersandi sebagai byte polos. Pertahankan /Encrypt dalam trailer baru tetapi tulis objek-objek update itu sendiri sebagai plaintext, dan kegagalannya sekadar bergerak satu langkah lebih belakangan: reader itu dengan benar mendeteksi enkripsi, menjalankan setiap objek yang disentuhnya lewat cipher file tersebut, termasuk yang baru yang tidak pernah dienkripsi sejak awal, dan mendapatkan noise kembali untuk konten yang sempurna terbaca sebelum dekripsi menyentuhnya. Salah satu kesalahan menghasilkan sebuah file yang terlihat seperti sebuah incremental update normal dan well-formed pada tingkat byte, persis sampai sebuah reader yang sesuai standar membukanya

Enam injector marker, satu gerbang enkripsi v2.14.2

PDFiumPas mengirimkan enam injector marker tingkat-byte, satu untuk setiap subset PDF ISO yang bisa dilabelinya: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1), dan PDF/VT-1 (ISO 16612-2). Masing-masing mengambil byte yang sudah ditulis FPDF_SaveAsCopy milik PDFium sendiri dan melapiskan sebuah incremental update kedua yang lebih kecil di atasnya: sebuah stream metadata XMP baru, sebuah edit dictionary katalog yang menunjuk padanya, dan untuk subset berorientasi-cetak sebuah OutputIntent dan profil ICC. Sejak v2.14.2, setiap satu dari InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, dan InjectPdfVTMarkers membaca trailer sumber lebih dulu, dan jika ia melaporkan sebuah entry /Encrypt yang sudah ada, menyalin sumber itu ke stream tujuan tanpa modifikasi dan langsung kembali. Tanpa XMP, tanpa OutputIntent, tanpa edit katalog — pemanggil mendapatkan kembali file asli, byte demi byte

var
  Src, Dst: TFileStream;
  Opts: TPdfXSaveOptions;
begin
  Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
  Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
  try
    Opts.Conformance := pxc4;
    InjectPdfXMarkers(Src, Dst, Opts);
    // pdfx-attempt.pdf is byte-identical to the source: still encrypted,
    // no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
    // nothing was corrupted either
  finally
    Dst.Free;
    Src.Free;
  end;
end;

Diizinkan terenkripsi tidak sama dengan aman untuk disisipi

PDF/E-1 dan PDF/R-1 keduanya secara eksplisit mengizinkan dokumen host mereka dienkripsi pada tingkat spesifikasi, yang terbaca seperti sebuah pengecualian sampai Anda melihat apa yang sebenarnya harus terjadi di disk. ISO 24517-1 §6.3 mengizinkan enkripsi untuk PDF/E-1, dan ISO 23504-1 §6.2.3 mengizinkannya untuk PDF/R-1 asalkan header mendeklarasikan %PDF-2.0. Tak satu pun klausul itu mengatakan apa pun tentang apakah sebuah post-processor tingkat-byte bisa dengan aman menambahkan sebuah objek plaintext ke container terenkripsi itu, dan tidak bisa, karena alasan §7.5.6 yang sama yang berlaku untuk setiap subset lainnya. Validator kesesuaian milik PDFiumPas sendiri untuk kedua profil ini, ValidatePdfECompliance dan ValidatePdfRCompliance, mencatat kehadiran /Encrypt dengan sengaja tanpa menandainya sebagai sebuah cacat, yang benar untuk sebuah validator read-only yang tidak pernah menulis satu byte pun. Ini juga sebuah pola yang mudah dilewati dan diasumsikan injector saudaranya tidak membutuhkan penjaga terpisah, padahal injector adalah satu fungsi dalam pasangan itu yang sebenarnya harus menolak

Apakah SaveAsPdfX diam-diam mendekripsi dokumen Anda?

Ya, kapan pun Anda melalui metode kenyamanan publik alih-alih memanggil sebuah injector secara langsung. Setiap satu dari TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, dan SaveAsPdfVT merender dokumen saat ini ke sebuah stream sementara dengan SaveAs(Tmp, saRemoveSecurity) sebelum menyerahkan byte itu ke injector-nya yang sesuai. saRemoveSecurity dipetakan ke flag FPDF_REMOVE_SECURITY milik PDFium sendiri, sehingga salinan sementara yang diterima injector tersebut sama sekali tidak pernah terenkripsi, dan penjaga /Encrypt milik injector tidak pernah punya alasan terpicu. Output-nya membawa marker PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, atau PDF/VT-1 Anda, tetapi tidak lagi dilindungi password apa pun yang membuka sumbernya

Trade-off itu tidak terlihat sampai seseorang di hilir membuka salinan arsip "terlindungi" tanpa password dan memperhatikan itu memang berfungsi. Perbaikannya bukan sebuah pemanggilan metode berbeda; PDFiumPas tidak memiliki padanan saAddSecurity untuk dipasangkan dengan saRemoveSecurity, karena mesin PDFium yang mendasarinya tidak pernah dibangun untuk menulis enkripsi baru, hanya untuk menghapusnya. Jika kedua properti itu penting untuk satu file, enkripsi harus menjadi langkah terpisah yang Anda miliki sendiri, diterapkan setelah marker kesesuaian, tidak dilipat ke dalam pemanggilan SaveAsPdfA yang sama

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';           // needed to open the source at all
    Pdf.FileName := 'signed-encrypted-invoice.pdf';
    Pdf.LoadDocument;
    Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
    // invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
    // ran first inside SaveAsPdfA: the output opens without a password
  finally
    Pdf.Free;
  end;
end;

Apa yang terjadi ketika Anda menandatangani sebuah PDF terenkripsi dengan PAdES?

PDFiumPas menolak secara langsung, alih-alih diam-diam membuang permintaan seperti yang dilakukan sebuah injector marker. TPdf.SignPades dan SignPadesToStream keduanya melalui sebuah SignPadesBytes internal, dan hal pertama yang dilakukannya setelah mem-parse trailer sumber adalah memeriksa /Encrypt. Jika entry itu hadir, ia memunculkan EPadesCrypto dengan pesan "SignPadesBytes: the source document is encrypted; remove encryption before signing" alih-alih melanjutkan lebih jauh. InjectPadesDssMarkers, fungsi yang menyematkan sertifikat, respons OCSP, dan CRL untuk validasi jangka-panjang, menerapkan pemeriksaan identik untuk alasan identik, dengan pesannya sendiri: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

Alasan di sini lebih ketat daripada pass-through milik injector marker, dan sengaja begitu. Sebuah pass-through diam-diam aman untuk sebuah cap PDF/A karena melewatinya meninggalkan Anda dengan PDF valid yang sama seperti awal, hanya tidak berlabel. Signing tidak bisa gagal sediam itu: sebuah signature yang diam-diam tidak pernah ditambahkan terlihat, bagi kode pemanggil mana pun yang hanya memeriksa sebuah hasil boolean, persis seperti sebuah signature yang berhasil ditambahkan. EPadesCrypto turun dari kelas Exception biasa, sehingga menangkapnya adalah exception handling normal, bukan sebuah konvensi control-flow khusus yang perlu Anda pelajari

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';
    Pdf.FileName := 'encrypted-contract.pdf';
    Pdf.LoadDocument;
    try
      Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
    except
      on E: EPadesCrypto do
        // E.Message: 'SignPadesBytes: the source document is encrypted;
        // remove encryption before signing'
        raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
    end;
  finally
    Pdf.Free;
  end;
end;

Mengurutkan cap kesesuaian, signature, dan enkripsi

Perbaikan praktisnya adalah pengurutan, bukan library yang berbeda. Terapkan marker PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, atau PDF/VT-1 lebih dulu, tambahkan signature PAdES apa pun berikutnya, dan hanya kemudian jalankan langkah apa pun dalam pipeline Anda yang benar-benar memiliki enkripsi, entah itu sebuah PDF writer khusus, sebuah appliance signing, atau implementasi AES Anda sendiri. Lapisan incremental-update milik PDFiumPas cocok secara alami di tengah urutan itu, menambahkan objek kecil dan bertarget ke sebuah file yang sudah selesai, dan enkripsi berada di akhir persis karena itu satu-satunya operasi dalam rantai tersebut yang tidak bisa dilakukan atau dibalik PDFiumPas sendiri

Tak satu pun dari ini mengubah bagaimana PDFiumPas membaca trailer dan data cross-reference yang menjadi ketergantungan setiap incremental update, yang merupakan sumber kehalusan tersendiri begitu stream xref masuk ke gambaran; memvalidasi object dan xref stream sebuah PDF membahas bagaimana jalur pembacaan-trailer yang sama itu menangani struktur terkompresi PDF 1.5+. Dan begitu sebuah dokumen siap untuk sesuatu yang lebih kuat dari sebuah cap kesesuaian, menandatangani sebuah PDF dengan signature PAdES B-B di Delphi adalah tempat SignPades mengambil alih persis dari titik artikel ini berhenti

Injector marker dan metode SignPades yang dijelaskan di sini disertakan sebagai bagian dari PDFium Component untuk Delphi dan C++Builder, berdampingan dengan rendering dan inspeksi read-only yang disediakan PDFium secara native