HotPDF šifruje PDF pro konkrétní držitele certifikátů přes ISO 32000 public-key security handler: EnablePubKeyEncryption bere 20bajtové náhodné seed a každý příjemce dostane vlastní CMS obálku, postavenou AddPubKeyRecipientCertificate pro RSA klíče (RSA-OAEP key transport) nebo AddPubKeyAgreementRecipientWithSecret pro eliptické křivky (ECDH na P-256, P-384, P-521, X25519 nebo X448). Nikdo nesdílí heslo; kdo drží odpovídající privátní klíč, ten soubor otevře
Use case je vždy nějaká varianta téhož příběhu. Kvartální audit balík jde třem externím recenzentům, legal chce, aby si ho každý přečetl, tisknout smí jen jeden a nikdo nechce heslo ležet v emailovém vlákně vedle přílohy. Heslové šifrování to neumí vyjádřit. Certifikátové šifrování umí, protože každý příjemce odemkne dokument klíčem, který už drží, a každý příjemce může nést jinou sadu oprávnění ve své vlastní obálce
Jak se šifrování PDF certifikáty liší od hesla?
PDF šifrované veřejným klíčem odvozuje svůj file key z náhodného seed plus přesných bajtů každé obálky příjemce, ne z ničeho, co člověk napíše. Handler popisuje ISO 32000-1 §7.6.4 (§7.6.5 v ISO 32000-2) a obálky jsou CMS struktury EnvelopedData podle RFC 5652. HotPDF zapisuje /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5; u AES-256 to znamená /V 5 a položku /DefaultCryptFilter pod /CF s /CFM /AESV3 a pole /Recipients bydlí uvnitř toho crypt filtru. Každá obálka šifruje 24 bajtů: 20bajtový seed následovaný 32bitovým permission slovem toho příjemce. Hodnota /P v šifrovacím slovníku je jen zástupný symbol, protože skutečná oprávnění cestují uvnitř každé obálky. Při načítání reader rozbalí jednu obálku, vyzvedne seed a hashuje ho spolu s každou obálkou v pořadí /Recipients (SHA-256 pro AES-256, SHA-1 pro starší šifry), aby znovu složil file key. Pokud se teprve rozhodujete mezi tímhle modelem a obyčejnými hesly, průvodce AES-256 heslovým šifrováním a permission příznaky pokrývá druhou stranu toho kompromisu
Zápis RSA příjemců přes EnablePubKeyEncryption
U RSA certifikátů zavolejte EnablePubKeyEncryption s aes256 a pak AddPubKeyRecipientCertificate jednou na každý DER kódovaný certifikát před BeginDoc. Helper postaví RSAES-OAEP obálku in-process s hodnotami THPDFRSAOAEPHash pro OAEP digest i MGF1 digest (rohSHA256, rohSHA384 nebo rohSHA512) a obsah obálky zašifruje 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); // přesně 20 bajtů, i pro AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // defaultní typ klíče je aes128
// Recenzent A smí tisknout; recenzent B jen číst a extrahovat
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;
Tři detaily v tom výpisu nesou tíhu. Za prvé délka seed je fixně 20 bajtů pro každý typ klíče, včetně AES-256; EnablePubKeyEncryption na jakoukoli jinou délku vyhodí výjimku. Za druhé EnablePubKeyEncryption defaultně nabízí aes128 a oba certifikátové helpery odmítnou běžet, pokud typ klíče není aes256, takže zapomenutí druhého argumentu vám dá výjimku „certificate envelopes require aes256“. Legacy šifry (k40, k128, aes128) pořád fungují, ale jen přes AddPubKeyRecipient s obálkou, kterou jste si postavili jinde. Za třetí AES-256 public-key šifrování je funkce PDF 2.0, takže HotPDF zvýší verzi dokumentu na 2.0 automaticky. S nastaveným StrictVersionLock na nižší verzi se EnablePubKeyEncryption vrátí bez zapnutí čehokoli a selhání se objeví až na dalším řádku jako „call EnablePubKeyEncryption first“. Přepnutí šifrování během inkrementálního updatu vyhodí EInvalidOpException hned
Přidávání ECDH příjemců: P-256, P-384, P-521, X25519 a X448
U eliptických křivek zapisuje AddPubKeyAgreementRecipientWithSecret CMS key-agreement příjemce (KeyAgreeRecipientInfo, strukturu KARI z RFC 5753 s X25519 a X448 profilem z RFC 8418) a spočítá ECDH shared secret in-process. Křivku vybíráte hodnotou THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 nebo pkasX448. Scheme se musí trefit na klíč v certifikátu, jinak volání vyhodí „Certificate key does not match the requested agreement scheme“. Pod kapotou dostane každá obálka čerstvý náhodný 32bajtový UKM, key-encryption key odvozenou stdDH KDF (SHA-256 pro P-256 a X25519, SHA-384 pro P-384, SHA-512 pro P-521 a X448) a AES-256 key wrap podle RFC 3394. Samotný shared secret pochází z čistě Pascal kódu křivek, bez zapojení platformního crypto providera; článek o čistě Pascal aritmetice NIST křivek vysvětluje, jak ta vrstva vznikla a verifikovala se. U Montgomery křivek se celý efemérní klíčový pár dá vygenerovat lokálně:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Čerstvý efemérní scalar na každou obálku; clamping proběhne uvnitř 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: smysluplné jen pro NIST křivky
finally
HPDFSecureClearBytes(Scalar);
end;
end;
NIST křivky žádají po volajícím víc. HotPDF dodává public-key helpery jen pro X25519 a X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), takže pro P-256, P-384 a P-521 vygenerujete efemérní klíčový pár vlastním nářadím a předáte big-endian scalar přesně velikosti tělesa (32, 48 nebo 66 bajtů) plus odpovídající uncompressed bod 0x04||X||Y jako OriginatorPublicKey. HotPDF validuje bod příjemce proti rovnici křivky, ale nedokáže zkontrolovat, že váš originator public key skutečně patří vašemu scalaru. Neshodné poloviny i tak vyprodukují dokonale well-formed obálku, kterou žádný příjemce neotevře, a proto round-trip načtení patří do vaší testovací sady, ne jen kontrola velikosti souboru
Proč záleží na pořadí /Recipients?
Pořadí /Recipients záleží, protože file key je digest přes seed a každou obálku v pořadí pole, takže writer i reader musí hashovat tytéž bajty ve stejném pořadí. HotPDF drží obálky v pořadí, v jakém je přidáte, a zapisuje je nezměněné, což znamená, že příjemce můžete přidávat v libovolném pořadí, ale nic po proudu nesmí to pole přeuspořádat, překódovat ani „vyčistit“. Většina skutečných bugů v tejhle oblasti byla variací na tohle téma, kdy obě strany hashovaly mírně odlišné bajty:
- Ukládání dynamických polí do
TListpřesAdddrží jen surový pointer, zatímco reference count zůstává u lokální proměnné. DalšíSetLengthbuffer uvolní a může ho znovu použít, takže každý slot skončil jako alias poslední obálky a multi-recipient soubory odvodily špatný klíč. Oprava je uložit vlastněnou kopii přesList.Add(Pointer(System.Copy(Bytes))) - Rozbalování obálek parsuje DER na místě a key-recovery pas původně hashoval ta samá živá pole. Reader teď pořídí snapshoty nedotčených kopií každé obálky, než se jich kterýkoli unwrap dotkne, a digest běží přes snapshoty
- Binární DER puštěný přes Unicode
TStringListdostane bajty od$80výš překódované code page, takže HotPDF ukládá obálky interně jako hex text - Šifrované a binární řetězce se musí zapisovat jako hex řetězce. Literal string je vystaven normalizaci konce řádku, kde CR, LF i CRLF se stanou jediným LF (ISO 32000-1 §7.3.4.2), a to potichu přepíše ciphertext. HotPDF emituje každou položku
/Recipientsjako hex řetězec a vynechává ji ze string šifrování, protože každý reader potřebuje obálky, než drží jakýkoli klíč - První bajt DER
BIT STRINGpočítá nepoužité bity a musí být nula pro bajtově zarovnané klíče. Nechání ho neinicializované poSetLengthzapsalo, co zrovna leželo na stacku, striktní unwrap odmítl originator klíč a soubor tak mohl občas odmítnout otevřít se zrovna tím klíčem, pro který se psal - Když tentýž klíč pořád nedekryptuje, porovnávejte vrstvu po vrstvě: file key, pak prefix ciphertextu (IV), pak object key, pak plaintext. Bug bydlí hned za první vrstvou, která se rozejde
Jak otevřete certifikátem šifrované PDF privátním klíčem?
K otevření certifikátem šifrovaného PDF registrujte privátní klíčový materiál, než zavoláte LoadFromFile, protože HotPDF vyzvedává file key během strukturálního pasu. RSA nebo EC klíč rozparsovaný HPDFParsePFX přiřaďte do PubSecKeyMaterial, další RSA klíče přidejte AddPubSecKeyMaterial a syrové ECDH scalary registrujte přes AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint) s konstantami HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 nebo HPDFOIDECP521. NIST křivky vyžadují vlastní uncompressed veřejný bod příjemce; Montgomery křivky ho ignorují
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);
// Volitelné: vybrat obálku přímo místo zkoušení všech
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = zkusit každou obálku v pořadí
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Bez callbacku zkusí HotPDF každou obálku proti každému registrovanému klíči: nejdřív primární klíč, pak každý další RSA klíč, pak EC materiál. PubSecRecipientQuery dostane počet obálek a vrací 0-based index nebo -1 a index mimo pole vyhodí výjimku místo seříznutí. Vezměte na vědomí, že AddPubSecKeyMaterial přijímá jen RSA materiál (trvá na modulusu a privátním exponentu), takže EC klíče patří do PubSecKeyMaterial nebo AddPubSecAgreementKeyMaterial. Když žádný klíč nerozbalí žádnou obálku, recovery krok se vrátí bez file key místo vyhození výjimky, takže ověřte, že obsah, který čekáte, se skutečně dekryptoval, místo důvěřovat tomu, že se volání načtení vrátilo
Co HotPDF negarantuje
HotPDF garantuje, že jeho vlastní writer a reader si rozumí bajt za bajt, a staví obálky následující výše citované CMS struktury. Negarantuje, že každý PDF prohlížeč otevře každou kombinaci. Podpora RSA-OAEP key transportu a X25519 nebo X448 příjemců se liší mezi čtečkami a verzemi a výsledky kompatibility pro ty kombinace jsme nepublikovali. Jestli musí dokument otevřít v konkrétním prohlížeči, zašifrujte testovací soubor testovacím certifikátem téhož typu klíče a otevřete ho tam, než se zavážete scheme. Oprávnění nesenená v obálce zůstávají politikou, kterou konformní software ctí, přesně jako pod heslovým šifrováním. Kvalita seed je taky vaše odpovědnost: AESGenerateRandomBytes je tu pro tuhle práci a HotPDF svou kopii seed smaže, jakmile se file key odvodí. Jestli potřebujete, aby řetězec, stream nebo příloha používaly jiný crypt filtr, průvodce politikami crypt filtrů pro StmF, StrF a EFF ukazuje, která jména filtrů public-key handler přijímá
Certifikátové šifrování, obálky příjemců RSA-OAEP a ECDH a načítání privátních klíčů všechny vycházejí v HotPDF Delphi PDF component, po boku heslového šifrování, digitálních podpisů a zbytku ISO 32000 toolsetu pro Delphi a C++Builder