HotPDF zašifruje PDF pre konkrétnych držiteľov certifikátov cez public-key security handler z ISO 32000: EnablePubKeyEncryption berie 20-bajtové náhodné semienko a každý príjemca dostane vlastnú CMS obálku, postavenú cez AddPubKeyRecipientCertificate pre RSA kľúče (key transport RSA-OAEP) alebo AddPubKeyAgreementRecipientWithSecret pre elipticko-krivkové kľúče (ECDH na P-256, P-384, P-521, X25519 alebo X448). Nikto nezdieľa heslo; kto drží zodpovedajúci privátny kľúč, otvorí súbor
Use case je vždy nejaká verzia toho istého príbehu. Štvrťročný audit balík ide trom externým recenzentom, právne oddelenie chce, aby si ho každý prečítal, tlačiť ho smie len jeden z nich a heslo ležať v mailovej konverzácii vedľa prílohy nechce nikto. Heslové šifrovanie to vyjadriť nedokáže. Certifikátové áno, lebo každý príjemca odomkne dokument kľúčom, ktorý už drží, a každý príjemca môže niesť inú sadu povolení vo svojej vlastnej obálke
Čím sa šifrovanie PDF certifikátmi líši od hesla?
PDF šifrované verejným kľúčom odvodzuje svoj file key z náhodného semienka plus presných bajtov každej príjemcovej obálky, nie z ničoho, čo napíše človek. Handler opisuje ISO 32000-1 §7.6.4 (§7.6.5 v ISO 32000-2) a obálky sú štruktúry CMS EnvelopedData podľa RFC 5652. HotPDF píše /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5; pri AES-256 to znamená /V 5 a položku /DefaultCryptFilter pod /CF s /CFM /AESV3 a pole /Recipients býva vnútri toho crypt filtra. Každá obálka šifruje 24 bajtov: 20-bajtové semienko nasledované 32-bitovým slovom povolení toho príjemcu. Hodnota /P v šifrovacom slovníku je len zástupný symbol, lebo skutočné povolenia cestujú vnútri každej obálky. Pri načítaní rozbalí čítač jednu obálku, získa semienko a hashuje semienko spolu s každou obálkou v poradí /Recipients (SHA-256 pre AES-256, SHA-1 pre staršie šifry), aby znovu zložil file key. Ak sa ešte rozhodujete medzi týmto modelom a obyčajnými heslami, sprievodca AES-256 heslovým šifrovaním a flagmi povolení pokrýva druhú stranu toho kompromisu
Zápis RSA príjemcov pomocou EnablePubKeyEncryption
Pre RSA certifikáty zavolajte EnablePubKeyEncryption s aes256 a potom AddPubKeyRecipientCertificate raz na každý DER-kódovaný certifikát pred BeginDoc. Helper stavia obálku RSAES-OAEP v procese s hodnotami THPDFRSAOAEPHash pre OAEP digest aj MGF1 digest (rohSHA256, rohSHA384 alebo rohSHA512) a obsah obálky š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); // presne 20 bajtov, aj pre AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // predvolený typ kľúča je aes128
// Recenzent A smie tlačiť; recenzent B len číta a extrahuje
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;
Tri detaily v tom výpise nesú váhu. Po prvé, dĺžka semienka je fixná na 20 bajtov pre každý typ kľúča, AES-256 vrátane; EnablePubKeyEncryption vyhodí výnimku pri akejkoľvek inej dĺžke. Po druhé, EnablePubKeyEncryption má predvolené aes128 a oba certifikátové helpery odmietnu bežať, pokiaľ typ kľúča nie je aes256, takže zabudnutý druhý argument vám dá výnimku „certificate envelopes require aes256". Staršie šifry (k40, k128, aes128) stále fungujú, ale len cez AddPubKeyRecipient s obálkou, ktorú si postavíte inde. Po tretie, public-key šifrovanie AES-256 je funkcia PDF 2.0, takže HotPDF zvýši verziu dokumentu na 2.0 automaticky. S StrictVersionLock nastaveným na nižšej verzii EnablePubKeyEncryption vráti bez povolenia čohokoľvek a zlyhanie sa ukáže až na ďalšom riadku ako „call EnablePubKeyEncryption first". Prepnutie šifrovania počas inkrementálnej aktualizácie vyhodí EInvalidOpException hneď
Pridávanie ECDH príjemcov: P-256, P-384, P-521, X25519 a X448
Pre elipticko-krivkové certifikáty AddPubKeyAgreementRecipientWithSecret zapíše key-agreement príjemcu CMS (KeyAgreeRecipientInfo, štruktúra KARI z RFC 5753, s profilom X25519 a X448 z RFC 8418) a spočíta ECDH zdieľané tajomstvo v procese. Krivku vyberiete hodnotou THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 alebo pkasX448. Schéma sa musí zhodovať s kľúčom v certifikáte, inak volanie vyhodí „Certificate key does not match the requested agreement scheme". Pod kapotou dostane každá obálka čerstvé náhodné 32-bajtové UKM, key-encryption key odvodenú stdDH KDF (SHA-256 pre P-256 a X25519, SHA-384 pre P-384, SHA-512 pre P-521 a X448) a AES-256 key wrap podľa RFC 3394. Samotné zdieľané tajomstvo pochádza z čistého Pascal kódu kriviek, bez zapojenia platformového crypto providera; článok o aritmetike NIST kriviek v čistom Pascale vysvetľuje, ako tá vrstva vznikla a overila sa. Pre Montgomery krivky sa celý dočasný kľúčový pár dá vygenerovať lokálne:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Čerstvý dočasný scalar na každú obálku; clamping prebehne vnútri rebríka
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: má zmysel len pri NIST krivkách
finally
HPDFSecureClearBytes(Scalar);
end;
end;
NIST krivky vyžadujú od volajúceho viac. HotPDF dodáva public-key helpery len pre X25519 a X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), takže pre P-256, P-384 a P-521 vygenerujete dočasný kľúčový pár vlastným nástroím a podáte scalar big-endian presne veľkosti poľa (32, 48 alebo 66 bajtov) plus zodpovedajúci nekomprimovaný bod 0x04||X||Y ako OriginatorPublicKey. HotPDF validuje bod príjemcu proti krivkovej rovnici, ale nevie skontrolovať, že váš originátor public key naozaj patrí k vášmu scalaru. Nesúrodné polovice aj tak vyrobia dokonale korektne vyzerajúcu obálku, ktorú neotvorí žiadny príjemca, a preto round-trip načítanie patrí do vašej testovacej sady, nielen kontrola veľkosti súboru
Prečo záleží na poradí /Recipients?
Poradie /Recipients záleží, lebo file key je digest cez semienko a každú obálku v poradí poľa, takže pisateľ aj čítač musia hashovať tie isté bajty v tej istej postupnosti. HotPDF drží obálky v poradí, v akom ich pridáte, a zapisuje ich nezmenené, takže príjemcov môžete pridávať v ľubovoľnom poradí, ale nič po prúde nesmie pole preskladať, prekódovať ani „upratať". Väčšina reálnych chýb v tej oblasti bola variáciou na tú tému, keď dve strany hashovali mierne odlišné bajty:
- Uloženie dynamických polí do
TListcezAdddrží len surový pointer, kým referenčný počet zostáva s lokálnou premennou. NasledujúceSetLengthuvoľní buffer a môže ho znovu použiť, takže každý slot skončil aliasom poslednej obálky a viacpríjemcové súbory odvodili zlý kľúč. Opravou je uložiť vlastnenú kópiu cezList.Add(Pointer(System.Copy(Bytes))) - Rozbaľovanie obálky parsuje DER na mieste a prieskum obnovy kľúča pôvodne hashoval tie isté živé polia. Čítač si teraz urobí snapshot nedotknutých kópií každej obálky skôr, než sa ich akékoľvek rozbalenie dotkne, a digest beží nad snapshotmi
- Binárny DER prehodený cez Unicode
TStringListdostane bajty od$80nahor prekódované code page, takže HotPDF ukladá obálky interne ako hex text - Šifrované a binárne reťazce sa musia zapisovať ako hex reťazce. Literal string podlieha normalizácii koncov riadkov, kde CR, LF aj CRLF sa stanú jedným LF (ISO 32000-1 §7.3.4.2), a to potichu prepíše ciphertext. HotPDF vypúšťa každú položku
/Recipientsako hex reťazec a vyňíma ju zo string šifrovania, keďže každý čítač potrebuje obálky, skôr než drží akýkoľvek kľúč - Prvý bajt DER
BIT STRINGpočíta nepoužité bity a musí byť nula pre bajtovo zarovnané kľúče. Nechanie neinicializované poSetLengthnapísalo čokoľvek, čo ležalo na zásobníku, a prísny unwrapper odmietol originátor kľúč, takže sa súbor občas odmietol otvoriť s tým istým kľúčom, pre ktorý bol napísaný - Keď ten istý kľúč stále nedokáže dešifrovať, porovnávajte vrstvu po vrstve: file key, potom prefix ciphertextu (IV), potom object key, potom plaintext. Chyba býva hneď za prvou vrstvou, ktorá sa nezhoduje
Ako otvoriť certifikátom šifrované PDF privátnym kľúčom?
Ak chcete otvoriť certifikátom šifrované PDF, zaregistrujte privátny kľúčový materiál pred volaním LoadFromFile, lebo HotPDF obnovuje file key počas štrukturálneho prieskumu. RSA alebo EC kľúč parsnutý cez HPDFParsePFX priraďte do PubSecKeyMaterial, ďalšie RSA kľúče pridajte cez AddPubSecKeyMaterial a surové ECDH scalare registrujte cez AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint) s konštami HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 alebo HPDFOIDECP521. NIST krivky vyžadujú vlastný nekomprimovaný public point príjemcu; Montgomery krivky 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);
// Voliteľné: vyberte obálku priamo namiesto skúšania všetkých
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = skúšaj každú obálku v poradí
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Bez callbacku skúša HotPDF každú obálku proti každému registrovanému kľúču: najprv primárny kľúč, potom každý ďalší RSA kľúč, potom EC materiál. PubSecRecipientQuery dostane počet obálok a vráti index od nuly alebo -1 a index mimo poľa vyhodí výnimku namiesto pritnutia na hranici. Pozor, AddPubSecKeyMaterial prijíma len RSA materiál (trvá na module a privátnom exponente), takže EC kľúče patria do PubSecKeyMaterial alebo AddPubSecAgreementKeyMaterial. Keď žiadny kľúč nerozbalí žiadnu obálku, krok obnovy sa vráti bez file key namiesto výnimky, takže overte, že obsah, ktorý očakávate, sa naozaj dešifroval, namiesto veriť tomu, že volanie načítania sa vrátilo
Čo HotPDF negarantuje
HotPDF garantuje, že jeho vlastný pisateľ a čítač sa zhodnú bajt za bajtom, a stavia obálky nasledujúce citované CMS štruktúry. Negarantuje, že každý PDF prehliadač otvorí každú kombináciu. Podpora key transportu RSA-OAEP a príjemcov X25519 či X448 sa líši medzi čítačmi a verziami a výsledky kompatibility pre tieto kombinácie nezverejňujeme. Ak musí dokument otvoriť sa v konkrétnom prehliadači, zašifrujte testovací súbor pre testovací certifikát rovnakého typu kľúča a otvorte ho tam, skôr než sa zaviazete ku schéme. Povolenia nesúce sa v obálke ostávajú politikou, ktorú konformný softvér rešpektuje, presne ako pri heslovom šifrovaní. Kvalita semienka je aj vaša zodpovednosť: AESGenerateRandomBytes je na tú prácu a HotPDF vymaže svoju kópiu semienka, keď sa file key odvodí. Ak potrebujete navyše, aby string, stream alebo príloha používali iný crypt filter, sprievodca politikami crypt filtrov pre StmF, StrF a EFF ukazuje, ktoré mená filtrov public-key handler prijíma
Certifikátové šifrovanie, obálky príjemcov RSA-OAEP a ECDH a načítanie privátnych kľúčov prichádzajú všetky v HotPDF Delphi PDF component, vedľa heslového šifrovania, digitálnych podpisov a zvyšku ISO 32000 toolsetu pre Delphi a C++Builder