HotPDF criptează un PDF pentru deținători de certificate anumiți prin handler-ul de securitate cu cheie publică din ISO 32000: EnablePubKeyEncryption primește un seed aleator de 20 de octeți, iar fiecare destinatar primește propriul envelope CMS, construit de AddPubKeyRecipientCertificate pentru chei RSA (transport de cheie RSA-OAEP) sau de AddPubKeyAgreementRecipientWithSecret pentru chei pe curbe eliptice (ECDH pe P-256, P-384, P-521, X25519 sau X448). Nimeni nu partajează o parolă; cine deține cheia privată potrivită deschide fișierul
Cazul de utilizare e întotdeauna câte o variantă a aceleiași povești. Un pachet de audit trimestrial merge la trei recenzori externi, juridicul vrea ca fiecare dintre ei să îl citească, doar unuia i se permite să îl tipărească, și nimeni nu vrea o parolă stând într-un fir de email lângă atașament. Criptarea cu parolă nu poate exprima asta. Criptarea cu certificate poate, pentru că fiecare destinatar deblochează documentul cu o cheie pe care o deține deja, iar fiecare destinatar poate cară un set de permisiuni diferit în propriul lui envelope
Cum diferă criptarea PDF pe bază de certificate de o parolă?
Un PDF criptat cu cheie publică își derivă file key-ul dintr-un seed aleator plus octeții exacți ai fiecărui envelope de destinatar, nu din nimic ce tastează o persoană. Handler-ul e descris în ISO 32000-1 §7.6.4 (§7.6.5 în ISO 32000-2), iar envelope-urile sunt structuri CMS EnvelopedData conform RFC 5652. HotPDF scrie /Filter /Adobe.PubSec cu /SubFilter /adbe.pkcs7.s5; pentru AES-256 înseamnă /V 5 și o intrare /DefaultCryptFilter sub /CF cu /CFM /AESV3, iar tabloul /Recipients trăiește în interiorul acelui crypt filter. Fiecare envelope criptează 24 de octeți: seed-ul de 20 de octeți urmat de cuvântul de permisiuni pe 32 de biți al destinatarului respectiv. Valoarea /P din dicționarul de criptare e doar un placeholder, pentru că permisiunile reale călătoresc în interiorul fiecărui envelope. La încărcare, un cititor desface un envelope, recuperează seed-ul și hash-uiește seed-ul împreună cu fiecare envelope în ordinea din /Recipients (SHA-256 pentru AES-256, SHA-1 pentru cifrurile mai vechi) ca să reconstruiască file key-ul. Dacă vă mai decideți între acest model și parolele obișnuite, ghidul de criptare AES-256 cu parolă și flag-uri de permisiuni acoperă cealaltă parte a compromisului
Scrierea destinatarilor RSA cu EnablePubKeyEncryption
Pentru certificate RSA, apelați EnablePubKeyEncryption cu aes256, apoi apelați AddPubKeyRecipientCertificate o dată per certificat codat DER înainte de BeginDoc. Helper-ul construiește un envelope RSAES-OAEP în proces, cu valori THPDFRSAOAEPHash pentru digest-ul OAEP și digest-ul MGF1 (rohSHA256, rohSHA384 sau rohSHA512), și criptează conținutul envelope-ului cu 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); // exact 20 de octeți, chiar și pentru AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // tipul de cheie implicit e aes128
// Reviewerul A poate tipări; reviewerul B poate doar citi și extrage
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;
Trei detalii din listarea aceea poartă greutatea. Întâi, lungimea seed-ului e fixată la 20 de octeți pentru orice tip de cheie, AES-256 inclus; EnablePubKeyEncryption ridică excepție la orice altă lungime. Al doilea, EnablePubKeyEncryption are implicit aes128, iar ambii helper-i de certificate refuză să ruleze dacă tipul de cheie nu e aes256, deci uitarea celui de-al doilea argument vă aduce excepția „certificate envelopes require aes256”. Cifrurile legacy (k40, k128, aes128) funcționează în continuare, dar doar prin AddPubKeyRecipient cu un envelope construit în altă parte. Al treilea, criptarea cu cheie publică AES-256 e o funcționalitate PDF 2.0, deci HotPDF ridică versiunea documentului la 2.0 automat. Cu StrictVersionLock setat pe o versiune mai mică, EnablePubKeyEncryption se întoarce fără să activeze nimic, iar eșecul apare abia pe rândul următor ca „call EnablePubKeyEncryption first”. Schimbarea criptării în timpul unei actualizări incrementale ridică EInvalidOpException pe loc
Adăugarea destinatarilor ECDH: P-256, P-384, P-521, X25519 și X448
Pentru certificate pe curbe eliptice, AddPubKeyAgreementRecipientWithSecret scrie un destinatar CMS de key-agreement (KeyAgreeRecipientInfo, structura KARI din RFC 5753, cu profilul X25519 și X448 din RFC 8418) și calculează secretul partajat ECDH în proces. Alegeți curba cu o valoare THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 sau pkasX448. Schema trebuie să se potrivească cu cheia din certificat, altfel apelul ridică „Certificate key does not match the requested agreement scheme”. Sub capotă, fiecare envelope primește un UKM aleator proaspăt de 32 de octeți, o cheie de criptare a cheii derivată cu KDF-ul stdDH (SHA-256 pentru P-256 și X25519, SHA-384 pentru P-384, SHA-512 pentru P-521 și X448) și un AES-256 key wrap conform RFC 3394. Secretul partajat în sine vine din cod Pascal pur de curbe, fără niciun provider criptografic de platformă implicat; articolul despre aritmetica NIST curve în Pascal pur explică cum a fost construit și verificat stratul acela. Pentru curbele Montgomery, întreaga pereche de chei efemere poate fi generată local:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Scalar efemer proaspăt per envelope; clamping-ul are loc în interiorul ladder-ului
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: are sens doar pentru curbele NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Curbele NIST cer mai multe de la apelant. HotPDF livrează helper-i de cheie publică doar pentru X25519 și X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), deci pentru P-256, P-384 și P-521 generați perechea de chei efemere cu uneltele voastre și pasați un scalar big-endian de exact dimensiunea câmpului (32, 48 sau 66 de octeți) plus punctul necomprimat potrivit 0x04||X||Y ca OriginatorPublicKey. HotPDF validează punctul destinatarului împotriva ecuației curbei, dar nu poate verifica că cheia voastră publică de originator aparține într-adevăr scalarului vostru. Jumătăți nepotrivite produc în continuare un envelope perfect bine format pe care niciun destinatar nu îl poate deschide, motiv pentru care un dus-întors de încărcare aparține suitei voastre de teste, nu doar unei verificări de mărime a fișierului
De ce contează ordinea din /Recipients?
Ordinea din /Recipients contează pentru că file key-ul e un digest peste seed și fiecare envelope în ordinea tabloului, deci scriitorul și cititorul trebuie să hash-uiească aceiași octeți în aceeași secvență. HotPDF păstrează envelope-urile în ordinea în care le adăugați și le scrie neschimbate, ceea ce înseamnă că puteți adăuga destinatari în orice ordine doriți, dar nimic din aval nu are voie să reordoneze, să re-encodeze sau să „curățe” tabloul acela. Majoritatea bug-urilor reale din zona aceasta au fost variații pe tema aceasta, cazuri în care două părți hash-uiau octeți ușor diferiți:
- Stocarea de tablouri dinamice într-un
TListprinAddpăstrează doar un pointer brut, în timp ce contorul de referințe rămâne la variabila locală. UrmătorulSetLengtheliberează buffer-ul și îl poate refolosi, deci fiecare slot ajungea să aliaseze ultimul envelope, iar fișierele multi-destinatar derivau cheia greșită. Reparația e să stocați o copie deținută cuList.Add(Pointer(System.Copy(Bytes))) - Desfășurarea envelope-urilor parsează DER-ul în loc, iar trecerea de recuperare a cheii hash-uia inițial aceleași tablouri vii. Cititorul face acum instantanee cu copii imaculate ale fiecărui envelope înainte ca orice unwrap să le atingă, iar digest-ul rulează peste instantanee
- DER-ul binar pasat printr-un
TStringListUnicode vede octeți de la$80în sus re-encodați de pagina de cod, deci HotPDF stochează envelope-urile intern ca text hexazecimal - Șirurile criptate și binare trebuie scrise ca hex strings. Un string literal e supus normalizării de sfârșit de linie, în care CR, LF și CRLF devin toate un singur LF (ISO 32000-1 §7.3.4.2), iar asta rescrie în tăcere ciphertext-ul. HotPDF emite fiecare intrare
/Recipientsca hex string și o exemptuează de la criptarea de șiruri, pentru că orice cititor are nevoie de envelope-uri înainte să dețină vreo cheie - Primul octet al unui
BIT STRINGDER numără biții nefolosiți și trebuie să fie zero pentru chei aliniate la octet. Lăsat neinițializat dupăSetLength, scria orice era pe stivă, iar un wrapper strict respingea cheia originatorului, deci un fișier putea ocazional să nu se deschidă cu exact cheia pentru care fusese scris - Când aceeași cheie tot nu poate decripta, comparați strat cu strat: file key-ul, apoi prefixul ciphertext-ului (IV-ul), apoi cheia de obiect, apoi plaintext-ul. Bug-ul stă chiar după primul strat care nu se potrivește
Cum deschideți un PDF criptat cu certificate folosind o cheie privată?
Pentru a deschide un PDF criptat cu certificate, înregistrați materialul de cheie privată înainte să apelați LoadFromFile, pentru că HotPDF recuperează file key-ul în timpul trecerii structurale. Atribuiți o cheie RSA sau EC parsată cu HPDFParsePFX către PubSecKeyMaterial, adăugați chei RSA suplimentare cu AddPubSecKeyMaterial și înregistrați scalari ECDH bruti cu AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), folosind constantele HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 sau HPDFOIDECP521. Curbele NIST cer punctul public necomprimat propriu al destinatarului; curbele Montgomery îl ignoră
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);
// Opțional: alegeți envelope-ul direct în loc să le încercați pe toate
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = încearcă fiecare envelope în ordine
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Fără callback, HotPDF încearcă fiecare envelope împotriva fiecărei chei înregistrate: cheia primară întâi, apoi fiecare cheie RSA suplimentară, apoi materialul EC. PubSecRecipientQuery primește numărul de envelope-uri și întoarce un index bazat pe zero sau -1, iar un index în afara tabloului ridică excepție în loc să fie decolnat. De reținut că AddPubSecKeyMaterial acceptă doar material RSA (se ține morțis de un modul și un exponent privat), deci cheile EC își au locul în PubSecKeyMaterial sau AddPubSecAgreementKeyMaterial. Când nicio cheie nu desface niciun envelope, pasul de recuperare se întoarce fără file key în loc să ridice excepție, deci verificați că conținutul așteptat s-a decriptat cu adevărat, nu doar că apelul de încărcare s-a întors
Ce nu garantează HotPDF
HotPDF garantează că propriile lui scriitor și cititor sunt de acord octet cu octet și construiește envelope-uri care urmează structurile CMS citate mai sus. Nu garantează că orice vizualizator PDF deschide orice combinație. Suportul pentru transport de cheie RSA-OAEP și pentru destinatari X25519 sau X448 variază între cititoare și versiuni, iar noi nu am publicat rezultate de compatibilitate pentru combinațiile acelea. Dacă un document trebuie să se deschidă într-un anumit vizualizator, criptați un fișier de test pentru un certificat de test al aceluiași tip de cheie și deschideți-l acolo înainte să vă angajați pe o schemă. Permisiunile cărate în envelope rămân o politică pe care software-ul conformant o respectă, exact ca la criptarea cu parolă. Calitatea seed-ului e și ea responsabilitatea voastră: AESGenerateRandomBytes e acolo pentru treaba asta, iar HotPDF își șterge copia seed-ului odată ce file key-ul a fost derivat. Dacă aveți nevoie ca un string, un stream sau un atașament să folosească un alt crypt filter, ghidul de politici crypt filter pentru StmF, StrF și EFF arată ce nume de filtre acceptă handler-ul cu cheie publică
Criptarea cu certificate, envelope-urile de destinatari RSA-OAEP și ECDH și încărcarea cheilor private se livrează în HotPDF Delphi PDF component, alături de criptarea cu parolă, semnăturile digitale și restul setului de unelte ISO 32000 pentru Delphi și C++Builder