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
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
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
TListlewatAddhanya menyimpan pointer mentah sementara reference count-nya tinggal di variabel lokal.SetLengthberikutnya 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 denganList.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
TStringListUnicode membuat byte bernilai$80ke 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
/Recipientssebagai hex string dan mengecualikannya dari enkripsi string, karena setiap reader butuh envelope-envelope itu sebelum memegang key apa pun - Byte pertama
BIT STRINGDER menghitung bit tak terpakai dan harus nol untuk key yang sejajar byte. Membiarkannya tak terinisialisasi setelahSetLengthmenulis 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
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