Artikel Teknis

Enkripsi Sertifikat PDF di Delphi: RSA-OAEP dan ECDH

HotPDF mengenkripsi PDF untuk pemegang sertifikat tertentu lewat public-key security handler ISO 32000: EnablePubKeyEncryption mengambil seed acak 20 byte, dan setiap penerima kebagian envelope CMS miliknya sendiri, dibangun oleh AddPubKeyRecipientCertificate untuk key RSA (key transport RSA-OAEP) atau AddPubKeyAgreementRecipientWithSecret untuk key kurva eliptik (ECDH di P-256, P-384, P-521, X25519, atau X448). Tak ada yang berbagi password; siapa pun yang memegang private key yang cocok membuka filenya

Use case-nya selalu versi lain dari cerita yang sama. Satu paket audit triwulan dikirim ke tiga reviewer eksternal, legal meminta semua mereka membacanya, hanya satu yang boleh mencetaknya, dan tak ada yang ingin password menganggur di utas email di samping lampirannya. Enkripsi password tak bisa mengekspresikan itu. Enkripsi sertifikat bisa, karena setiap penerima membuka dokumennya dengan key yang sudah mereka pegang, dan setiap penerima bisa membawa set permission berbeda di dalam envelope miliknya sendiri

Apa bedanya enkripsi PDF berbasis sertifikat dengan password?

PDF terenkripsi public-key menurunkan file key-nya dari seed acak plus byte persis dari setiap envelope penerima, bukan dari apa pun yang diketik manusia. Handler ini dijelaskan di ISO 32000-1 §7.6.4 (§7.6.5 di ISO 32000-2), dan envelope-nya adalah struktur CMS EnvelopedData sebagaimana didefinisikan RFC 5652. HotPDF menulis /Filter /Adobe.PubSec dengan /SubFilter /adbe.pkcs7.s5; untuk AES-256 artinya /V 5 dan entri /DefaultCryptFilter di bawah /CF dengan /CFM /AESV3, dan array /Recipients berada di dalam crypt filter itu. Setiap envelope mengenkripsi 24 byte: seed 20 byte disusul permission word 32-bit milik penerima itu. Nilai /P di dictionary enkripsi hanyalah placeholder, karena permission sesungguhnya berjalan di dalam setiap envelope. Saat dimuat, reader membuka satu envelope, memulihkan seed-nya, dan meng-hash seed itu bersama setiap envelope dalam urutan /Recipients (SHA-256 untuk AES-256, SHA-1 untuk cipher yang lebih tua) untuk membangun ulang file key. Kalau Anda masih mengimbangi model ini dengan password biasa, panduan enkripsi password AES-256 dan flag permission membahas sisi lain dari trade-off itu

Diagram enkripsi public-key HotPDF: EnablePubKeyEncryption mematok seed 20 byte, setiap envelope CMS EnvelopedData mengenkripsi 20 byte itu plus satu permission word 32-bit di dalam /Filter /Adobe.PubSec dengan /SubFilter /adbe.pkcs7.s5 dan /CFM /AESV3, dan reader membuka satu envelope, memulihkan seed lalu meng-hash-nya bersama setiap entri /Recipients dalam urutan array untuk membangun ulang file key
Nilai /P di dictionary enkripsi hanyalah placeholder karena permission sesungguhnya berjalan di dalam setiap envelope, dan tak ada pihak hilir yang boleh mengurutkan ulang atau mengenkode ulang array yang dilintasi digest

Menulis penerima RSA dengan EnablePubKeyEncryption

Untuk sertifikat RSA, panggil EnablePubKeyEncryption dengan aes256, lalu panggil AddPubKeyRecipientCertificate sekali per sertifikat terenkode DER sebelum BeginDoc. Helper ini membangun envelope RSAES-OAEP in-process dengan nilai THPDFRSAOAEPHash untuk digest OAEP dan digest MGF1 (rohSHA256, rohSHA384, atau rohSHA512), dan mengenkripsi isi envelope dengan AES-256-CBC

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;

procedure WriteAuditPack(const OutFile: string);
var
  Pdf: THotPDF;
  Seed: AnsiString;
begin
  SetLength(Seed, 20);                      // tepat 20 byte, juga untuk AES-256
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // tipe key bawaannya aes128
    // Reviewer A boleh mencetak; reviewer B hanya boleh baca dan ekstrak
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
      [prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
      [prExtractContent]);
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Tiga detail di listing itu menopang beban. Pertama, panjang seed dipatok 20 byte untuk setiap tipe key, termasuk AES-256; EnablePubKeyEncryption melempar exception pada panjang lain. Kedua, EnablePubKeyEncryption bawaannya aes128, dan kedua helper sertifikat menolak berjalan kecuali tipe key-nya aes256, jadi lupa argumen kedua membuat Anda kebagian exception "certificate envelopes require aes256". Cipher warisan (k40, k128, aes128) masih bekerja, tapi hanya lewat AddPubKeyRecipient dengan envelope yang Anda bangun di tempat lain. Ketiga, enkripsi public-key AES-256 adalah fitur PDF 2.0, jadi HotPDF menaikkan versi dokumen ke 2.0 otomatis. Dengan StrictVersionLock terpasang pada versi lebih rendah, EnablePubKeyEncryption kembali tanpa mengaktifkan apa pun, dan kegagalannya baru tampak di baris berikutnya sebagai "call EnablePubKeyEncryption first". Mengganti enkripsi di tengah update inkremental langsung melempar EInvalidOpException

Menambah penerima ECDH: P-256, P-384, P-521, X25519 dan X448

Untuk sertifikat kurva eliptik, AddPubKeyAgreementRecipientWithSecret menulis penerima key-agreement CMS (KeyAgreeRecipientInfo, struktur KARI dari RFC 5753, dengan profil X25519 dan X448 dari RFC 8418) dan menghitung shared secret ECDH in-process. Anda memilih kurvanya dengan nilai THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519, atau pkasX448. Scheme-nya harus cocok dengan key di sertifikat, atau panggilannya melempar "Certificate key does not match the requested agreement scheme". Di balik layar, setiap envelope kebagian UKM acak 32 byte yang segar, key-encryption key yang diturunkan dengan KDF stdDH (SHA-256 untuk P-256 dan X25519, SHA-384 untuk P-384, SHA-512 untuk P-521 dan X448), dan key wrap AES-256 sebagaimana didefinisikan RFC 3394. Shared secret-nya sendiri datang dari kode kurva Pascal murni, tanpa crypto provider platform yang terlibat; artikel aritmetika kurva NIST Pascal murni menjelaskan bagaimana lapisan itu dibangun dan diverifikasi. Untuk kurva Montgomery, seluruh pasangan kunci ephemeris bisa dibuat secara lokal:

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
  HPDFKeyAgreement;

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Scalar ephemeris segar per envelope; clamping terjadi di dalam ladder
  SetLength(Scalar, 32);
  AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
  try
    OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
    Pdf.AddPubKeyAgreementRecipientWithSecret(
      TFile.ReadAllBytes('legal-x25519.cer'),
      [prPrint, prExtractContent], pkasX25519,
      OriginatorPublic, Scalar,
      []);   // OwnPublicPoint: hanya berarti untuk kurva NIST
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

Kurva NIST menuntut lebih dari pemanggilnya. HotPDF hanya mengirim helper public-key untuk X25519 dan X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), jadi untuk P-256, P-384, dan P-521 Anda membuat pasangan kunci ephemeris dengan tooling sendiri dan mengirim scalar big-endian berukuran tepat ukuran field (32, 48, atau 66 byte) plus titik uncompressed 0x04||X||Y yang cocok sebagai OriginatorPublicKey. HotPDF memvalidasi titik penerima terhadap persamaan kurvanya, tapi ia tak bisa memeriksa bahwa public key originator Anda benar-benar milik scalar Anda. Dua belahan yang tak cocok tetap menghasilkan envelope yang bentuknya sempurna tapi tak bisa dibuka penerima mana pun, dan itulah kenapa load round-trip wajib masuk test suite Anda, bukan sekadar cek ukuran file

Diagram key agreement ECDH HotPDF: AddPubKeyAgreementRecipientWithSecret menurunkan shared secret dengan kode kurva Pascal murni, mencampur UKM 32 byte segar lewat KDF stdDH dengan SHA-256 untuk P-256 dan X25519, SHA-384 untuk P-384, SHA-512 untuk P-521 dan X448, lalu membungkus content key dengan key wrap AES-256 RFC 3394 untuk membangun envelope KeyAgreeRecipientInfo
Nilai scheme dari pkasECDHP256 sampai pkasX448 harus cocok dengan key sertifikatnya, dan belahan scalar serta public point yang tak cocok tetap menghasilkan envelope yang terbentuk rapi tapi tak bisa dibuka penerima mana pun

Kenapa urutan /Recipients itu penting?

Urutan /Recipients penting karena file key adalah digest atas seed dan setiap envelope dalam urutan array, jadi penulis dan reader harus meng-hash byte yang sama dalam urutan yang sama. HotPDF menyimpan envelope dalam urutan Anda menambahkannya dan menulisnya apa adanya, artinya Anda bebas menambah penerima dalam urutan apa pun, tapi tak ada pihak hilir yang boleh mengurutkan ulang, mengenkode ulang, atau "membereskan" array itu. Sebagian besar bug nyata di area ini adalah variasi dari tema itu, di mana dua sisi meng-hash byte yang sedikit berbeda:

  • Menyimpan dynamic array ke TList lewat Add hanya menyimpan pointer mentah sementara reference count-nya tinggal di variabel lokal. SetLength berikutnya membebaskan buffer itu dan bisa memakainya ulang, jadi setiap slot berujung alias ke envelope terakhir dan file multi-penerima menurunkan key yang salah. Perbaikannya adalah menyimpan salinan yang dimiliki dengan List.Add(Pointer(System.Copy(Bytes)))
  • Pembukaan envelope mem-parse DER di tempatnya, dan tahap pemulihan key semula meng-hash array-array hidup yang sama itu. Reader kini mengambil snapshot salinan murni dari setiap envelope sebelum pembukaan mana pun menyentuhnya, dan digest berjalan di atas snapshot-snapshot itu
  • DER biner yang lewat TStringList Unicode membuat byte bernilai $80 ke atas terenkode ulang oleh code page, jadi HotPDF menyimpan envelope sebagai teks hex di dalamnya
  • String terenkripsi dan biner harus ditulis sebagai hex string. String literal terkena normalisasi akhir baris, tempat CR, LF, dan CRLF semuanya menjadi satu LF (ISO 32000-1 §7.3.4.2), dan itu menulis ulang ciphertext diam-diam. HotPDF mengeluarkan setiap entri /Recipients sebagai hex string dan mengecualikannya dari enkripsi string, karena setiap reader butuh envelope-envelope itu sebelum memegang key apa pun
  • Byte pertama BIT STRING DER menghitung bit tak terpakai dan harus nol untuk key yang sejajar byte. Membiarkannya tak terinisialisasi setelah SetLength menulis apa pun yang kebetulan ada di stack, dan unwrapper yang ketat menolak key originator-nya, jadi sebuah file kadang gagal dibuka dengan key yang memang ditulis untuknya
  • Ketika key yang sama tetap tak bisa mendekripsi, bandingkan lapis demi lapis: file key, lalu prefiks ciphertext (IV-nya), lalu object key, lalu plaintext-nya. Bug-nya tinggal tepat setelah lapisan pertama yang tak cocok

Bagaimana membuka PDF terenkripsi sertifikat dengan private key?

Untuk membuka PDF terenkripsi sertifikat, daftarkan materi private key sebelum memanggil LoadFromFile, karena HotPDF memulihkan file key saat tahap struktural. Assign key RSA atau EC yang di-parse dengan HPDFParsePFX ke PubSecKeyMaterial, tambahkan key RSA lainnya dengan AddPubSecKeyMaterial, dan daftarkan scalar ECDH mentah dengan AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), memakai konstanta HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384, atau HPDFOIDECP521. Kurva NIST mensyaratkan public point uncompressed milik penerima itu sendiri; kurva Montgomery mengabaikannya

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;

procedure OpenAuditPack(const LegalScalar: TBytes);
var
  Reader: THotPDF;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    Reader.PubSecKeyMaterial :=
      HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
    Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
    // Opsional: pilih envelope-nya langsung alih-alih mencoba semuanya
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = coba setiap envelope berurutan
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Tanpa callback, HotPDF mencoba setiap envelope terhadap setiap key terdaftar: key utama lebih dulu, lalu setiap key RSA tambahan, lalu materi EC-nya. PubSecRecipientQuery menerima jumlah envelope dan mengembalikan indeks berbasis nol atau -1, dan indeks di luar array melempar exception alih-alih dicap. Perhatikan bahwa AddPubSecKeyMaterial hanya menerima materi RSA (ia bersikeras pada modulus dan private exponent), jadi key EC tempatnya di PubSecKeyMaterial atau AddPubSecAgreementKeyMaterial. Ketika tak ada key yang membuka envelope mana pun, tahap pemulihan kembali tanpa file key alih-alih melempar exception, jadi pastikan konten yang Anda harapkan benar-benar terdekripsi alih-alih percaya begitu saja panggilan load-nya kembali

Diagram pemuatan private key HotPDF: PubSecKeyMaterial membawa key utama RSA atau EC dari HPDFParsePFX, AddPubSecKeyMaterial hanya menambah key RSA, AddPubSecAgreementKeyMaterial mendaftarkan scalar ECDH mentah di bawah OID kurva HPDFOIDX25519 sampai HPDFOIDP521, dan saat LoadFromFile provider mencoba key utama, lalu setiap key RSA tambahan, lalu materi EC terhadap setiap envelope
Ketika tak ada key yang membuka envelope mana pun, tahap pemulihan kembali tanpa file key alih-alih melempar exception, jadi pastikan kontennya benar-benar terdekripsi atau patok envelope-nya lewat PubSecRecipientQuery

Apa yang tidak dijamin HotPDF

HotPDF menjamin writer dan reader-nya sendiri sepakat byte demi byte, dan membangun envelope yang mengikuti struktur CMS yang dikutip di atas. Ia tidak menjamin setiap PDF viewer membuka setiap kombinasi. Dukungan untuk key transport RSA-OAEP dan untuk penerima X25519 atau X448 bervariasi antar reader dan antar versi, dan kami belum menerbitkan hasil kompatibilitas untuk kombinasi-kombinasi itu. Kalau sebuah dokumen harus terbuka di viewer tertentu, enkripsi satu file uji untuk satu sertifikat uji bertipe key sama dan buka di sana sebelum Anda berkomitmen pada sebuah scheme. Permission yang dibawa envelope tetap merupakan kebijakan yang dihormati software yang patuh, persis seperti pada enkripsi password. Kualitas seed juga tanggung jawab Anda: AESGenerateRandomBytes ada untuk pekerjaan itu, dan HotPDF menghapus salinan seednya begitu file key turun. Kalau Anda juga butuh string, stream, atau lampiran memakai crypt filter yang berbeda, panduan kebijakan crypt filter untuk StmF, StrF, dan EFF menunjukkan nama filter mana yang diterima handler public-key

Enkripsi sertifikat, envelope penerima RSA-OAEP dan ECDH, serta pemuatan private key semuanya ikut terkirim di HotPDF Delphi PDF component, bersama enkripsi password, digital signature, dan sisa toolset ISO 32000 untuk Delphi dan C++Builder