HotPDF šifruje PDF za konkretne nosioce sertifikata kroz ISO 32000 public-key security handler: EnablePubKeyEncryption uzima 20-bajtni nasumični seed, a svaki primalac dobija svoj CMS envelope, koji gradi AddPubKeyRecipientCertificate za RSA ključeve (RSA-OAEP key transport) ili AddPubKeyAgreementRecipientWithSecret za eliptične krive (ECDH na P-256, P-384, P-521, X25519 ili X448). Niko ne deli lozinku; ko drži odgovarajući privatni ključ, taj otvara fajl
Slučaj upotrebe je uvek neka verzija iste priče. Tromesečni audit paket ide trojici spoljnih recenzenta, pravno odeljenje želi da ga svaki od njih pročita, samo jedan od njih sme da ga štampa, i niko neće lozinku koja sedi u mejl lancu pored priloga. Šifrovanje lozinkom to ne može da izrazi. Šifrovanje sertifikatom može, jer svaki primalac otključava dokument ključem koji već drži, i svaki primalac može nositi drugačiji skup dozvola unutar sopstvenog envelope-a
Po čemu se šifrovanje PDF-a sertifikatom razlikuje od lozinke?
PDF šifrovan javnim ključem izvodi svoj file key iz nasumičnog seed-a plus tačnih bajtova svakog envelope-a primalaca, a ne iz ičega što čovek otkuca. Handler je opisan u ISO 32000-1 §7.6.4 (§7.6.5 u ISO 32000-2), a envelope-i su CMS EnvelopedData strukture kako ih definiše RFC 5652. HotPDF upisuje /Filter /Adobe.PubSec sa /SubFilter /adbe.pkcs7.s5; za AES-256 to znači /V 5 i /DefaultCryptFilter unos pod /CF sa /CFM /AESV3, a /Recipients niz živi unutar tog crypt filtera. Svaki envelope šifruje 24 bajta: 20-bajtni seed pa 32-bitni permission word tog primalca. /P vrednost u encryption rečniku je samo rezervisano mesto, jer prave dozvole putuju unutar svakog envelope-a. Pri učitavanju čitač odmotava jedan envelope, povrati seed, i hešuje seed zajedno sa svakim envelope-om u redosledu /Recipients (SHA-256 za AES-256, SHA-1 za starije šifre) da ponovo izgradi file key. Ako još uvek birate između ovog modela i običnih lozinki, vodič o AES-256 šifrovanju lozinkom i permission flagovima pokriva drugu stranu tog kompromisa
Upisivanje RSA primalaca pomoću EnablePubKeyEncryption
Za RSA sertifikate, pozovite EnablePubKeyEncryption sa aes256, pa pozovite AddPubKeyRecipientCertificate jednom po DER-enkodovanom sertifikatu pre BeginDoc. Helper gradi RSAES-OAEP envelope u samom procesu sa THPDFRSAOAEPHash vrednostima za OAEP digest i MGF1 digest (rohSHA256, rohSHA384 ili rohSHA512), i šifruje sadržaj envelope-a sa 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); // tačno 20 bajtova, čak i za AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // podrazumevani tip ključa je aes128
// Recenzent A sme da štampa; recenzent B sme samo da čita i izvlači
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 detalja u tom ispisu nose teret. Prvo, dužina seed-a je fiksirana na 20 bajtova za svaki tip ključa, pa i AES-256; EnablePubKeyEncryption podiže izuzetak na bilo koju drugu dužinu. Drugo, EnablePubKeyEncryption po podrazumevanju ide na aes128, i oba sertifikat helpera odbijaju da rade osim ako je tip ključa aes256, pa zaboravljeni drugi argument donosi izuzetak „certificate envelopes require aes256”. Legacy šifre (k40, k128, aes128) i dalje rade, ali samo kroz AddPubKeyRecipient sa envelope-om koji ste izgradili negde drugde. Treće, AES-256 public-key šifrovanje je mogućnost PDF 2.0, pa HotPDF automatski podiže verziju dokumenta na 2.0. Sa postavljenim StrictVersionLock-om na nižoj verziji, EnablePubKeyEncryption se vrati bez uključivanja ičega, a kvar se pojavi tek u sledećoj liniji kao „call EnablePubKeyEncryption first”. Prelazak na drugu šifru tokom inkrementalnog ažuriranja odmah podiže EInvalidOpException
Dodavanje ECDH primalaca: P-256, P-384, P-521, X25519 i X448
Za sertifikate sa eliptičnim krivama, AddPubKeyAgreementRecipientWithSecret upisuje CMS key-agreement primalca (KeyAgreeRecipientInfo, KARI struktura iz RFC 5753, sa X25519 i X448 profilom iz RFC 8418) i računa ECDH deljeni tajni u samom procesu. Krivu birate THPDFPubKeyAgreementScheme vrednošću: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ili pkasX448. Šema mora da se poklopi sa ključem u sertifikatu, ili poziv podiže „Certificate key does not match the requested agreement scheme”. Ispod haube, svaki envelope dobija svež nasumični 32-bajtni UKM, key-encryption key izveden stdDH KDF-om (SHA-256 za P-256 i X25519, SHA-384 za P-384, SHA-512 za P-521 i X448), i AES-256 key wrap kako ga definiše RFC 3394. Sam deljeni tajni dolazi iz čistog Pascal koda za krive, bez ikakvog platformskog crypto provajdera; članak o čisto Pascal aritmetici NIST krivih objašnjava kako je taj sloj izgrađen i verifikovan. Za Montgomery krive ceo efemerni par ključeva može da se generiše lokalno:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Svež efemerni skalar po envelope-u; klamping radi unutar ladder-a
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: značajan samo za NIST krive
finally
HPDFSecureClearBytes(Scalar);
end;
end;
NIST krive traže više od pozivaoca. HotPDF isporučuje public-key helper-e samo za X25519 i X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), pa za P-256, P-384 i P-521 sami generišete efemerni par ključeva svojim alatima i predajete big-endian skalar tačno veličine polja (32, 48 ili 66 bajtova) plus odgovarajuću nekompresovanu 0x04||X||Y tačku kao OriginatorPublicKey. HotPDF validira tačku primalca naspram jednačine krive, ali ne može da proveri da vaš originator javni ključ zaista pripada vašem skalaru. Nepoklopljene polovine i dalje proizvode savršeno dobro oblikovan envelope koji nijedan primalac ne može da otvori, pa round-trip učitavanje spada u vaš test suite, a ne samo provera veličine fajla
Zašto je redosled /Recipients važan?
Redosled /Recipients je važan jer je file key digest preko seed-a i svakog envelope-a po redosledu niza, pa pisac i čitač moraju da hešuju iste bajtove u istom nizu. HotPDF čuva envelope-e u redosledu kojem ih dodajete i upisuje ih nepromenjene, što znači da primalace možete dodavati u bilo kom redosledu, ali ništa nizvodno ne sme da preuredi, ponovo enkoduje ili „sredi” taj niz. Većina pravih bugova u ovoj oblasti bila je varijanta na tu temu, gde su dve strane hešovale malo drugačije bajtove:
- Čuvanje dinamičkih nizova u
TListprekoAddzadržava samo sirovi pokazivač dok brojač referenci ostaje uz lokalnu promenljivu. SledećiSetLengthoslobađa bafer i može ga iskoristiti ponovo, pa je svaki slot završio kao alias poslednjeg envelope-a i fajlovi sa više primalaca izvodili su pogrešan ključ. Popravka je da se čuva vlasnička kopija saList.Add(Pointer(System.Copy(Bytes))) - Odmotavanje envelope-a parsira DER na mestu, i key-recovery prolaz je prvobitno hešovao te iste žive nizove. Čitač sada snima netaknute kopije svakog envelope-a pre nego što bilo koje odmotavanje dotakne, i digest ide preko snimaka
- Binarni DER proveden kroz Unicode
TStringListdobija bajtove od$80naviše ponovo enkodirane po code page, pa HotPDF čuva envelope-e interno kao heks tekst - Šifrovani i binarni stringovi moraju da se upišu kao heks stringovi. Literalan string podleže end-of-line normalizaciji gde CR, LF i CRLF svi postaju jedan LF (ISO 32000-1 §7.3.4.2), i to tiho prepisuje ciphertext. HotPDF emituje svaki
/Recipientsunos kao heks string i izuzima ga iz string šifrovanja, jer svakom čitaču trebaju envelope-i pre nego što drži ijedan ključ - Prvi bajt DER
BIT STRING-a broji nekorišćene bitove i mora biti nula za bajtno poravnate ključeve. Neinicijalizovan posleSetLengthupisivao je šta god je bilo na steku, i strog odmotavač je odbijao originator ključ, pa fajl povremeno nije mogao da se otvori baš ključem za koji je napisan - Kad isti ključ i dalje ne može da dešifruje, poredajte sloj po sloj: file key, pa ciphertext prefiks (IV), pa object key, pa čist tekst. Bug sedi odmah iza prvog sloja koji se ne složi
Kako otvoriti sertifikatom šifrovan PDF privatnim ključem?
Da otvorite PDF šifrovan sertifikatom, registrujte materijal privatnog ključa pre poziva LoadFromFile, jer HotPDF povrati file key tokom strukturnog prolaza. RSA ili EC ključ parsiran sa HPDFParsePFX dodelite PubSecKeyMaterial-u, dodatne RSA ključeve dodajte sa AddPubSecKeyMaterial, a sirove ECDH skalare registrujte sa AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), koristeći konstante HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ili HPDFOIDECP521. NIST krive zahtevaju sopstvenu nekompresovanu javnu tačku primalca; Montgomery krive je ignorišu
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);
// Opciono: izaberite envelope direktno umesto da isprobavate sve
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = isprobaj svaki envelope po redu
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Bez callback-a, HotPDF isprobava svaki envelope naspram svakog registrovanog ključa: prvo primarni ključ, pa svaki dodatni RSA ključ, pa EC materijal. PubSecRecipientQuery dobija broj envelope-a i vraća indeks sa bazom nula ili -1, a indeks van niza podiže izuzetak umesto da bude prikovan na granicu. Imajte na umu da AddPubSecKeyMaterial prihvata samo RSA materijal (inistiše na modulu i privatnom eksponentu), pa EC ključevi pripadaju PubSecKeyMaterial-u ili AddPubSecAgreementKeyMaterial-u. Kada nijedan ključ ne odmotava nijedan envelope, korak oporavka se vrati bez file key-a umesto da podigne izuzetak, pa proverite da se sadržaj koji očekujete zaista dešifrovao, umesto da verujete tome što je poziv učitavanja vratio
Šta HotPDF ne garantuje
HotPDF garantuje da se njegov pisac i čitač slažu bajt po bajt, i gradi envelope-e koji prate gore navedene CMS strukture. Ne garantuje da će svaki PDF pregledač otvoriti svaku kombinaciju. Podrška za RSA-OAEP key transport i za X25519 ili X448 primalce varira između čitača i verzija, i nismo objavili rezultate kompatibilnosti za te kombinacije. Ako dokument mora da se otvori u konkretnom pregledaču, šifrujte test fajl za test sertifikat istog tipa ključa i otvorite ga tamo pre nego što se opredelite za šemu. Dozvole koje envelope nosi ostaju politika koju usaglašen softver poštuje, tačno kao kod šifrovanja lozinkom. Kvalitet seed-a je i vaša odgovornost: AESGenerateRandomBytes postoji baš za taj posao, a HotPDF briše svoju kopiju seed-a čim se file key izvede. Ako stringu, streamu ili prilogu treba drugačiji crypt filter, vodič o crypt filter politikama za StmF, StrF i EFF pokazuje koja imena filtera public-key handler prihvata
Šifrovanje sertifikatom, RSA-OAEP i ECDH envelope-i primalaca i učitavanje privatnog ključa isporučuju se u HotPDF Delphi PDF component-i, uz šifrovanje lozinkom, digitalne potpise i ostatak ISO 32000 alata za Delphi i C++Builder