HotPDF krypterar en PDF för specifika certifikatinnehavare genom ISO 32000:s public-key security handler: EnablePubKeyEncryption tar ett slumpmässigt frö på 20 byte, och varje mottagare får sitt eget CMS-kuvert, byggt av AddPubKeyRecipientCertificate för RSA-nycklar (RSA-OAEP key transport) eller AddPubKeyAgreementRecipientWithSecret för elliptiska kurvnycklar (ECDH på P-256, P-384, P-521, X25519 eller X448). Ingen delar ett lösenord; den som håller en matchande privat nyckel öppnar filen
Användningsfallet är alltid någon variant av samma historia. Ett kvartals revisionspaket går till tre externa granskare, juristen vill att var och en av dem läser det, bara en av dem får skriva ut det, och ingen vill ha ett lösenord liggande i en e-posttråd bredvid bilagan. Lösenordskryptering kan inte uttrycka det. Certifikatkryptering kan det, eftersom varje mottagare låser upp dokumentet med en nyckel hen redan håller, och varje mottagare kan bära en annan behörighetsuppsättning inuti sitt eget kuvert
Hur skiljer sig certifikatsbaserad PDF-kryptering från ett lösenord?
En public-key-krypterad PDF härleder sin filnyckel från ett slumpmässigt frö plus de exakta bytena av varje mottagarkuvert, inte från något en människa skriver. Handlern beskrivs i ISO 32000-1 §7.6.4 (§7.6.5 i ISO 32000-2), och kuverten är CMS-EnvelopedData-strukturer enligt RFC 5652. HotPDF skriver /Filter /Adobe.PubSec med /SubFilter /adbe.pkcs7.s5; för AES-256 betyder det /V 5 och en /DefaultCryptFilter-post under /CF med /CFM /AESV3, och /Recipients-arrayen bor inuti det crypt-filtret. Varje kuvert krypterar 24 byte: fröet på 20 byte följt av den mottagarens 32-bitars behörighetsord. /P-värdet i krypteringsordboken är bara en platshållare, för att de riktiga behörigheterna färdas inuti varje kuvert. Vid inläsning vecklar en läsare upp ett kuvert, återvinner fröet och hashar fröet tillsammans med varje kuvert i /Recipients-ordning (SHA-256 för AES-256, SHA-1 för de äldre chifrren) för att bygga om filnyckeln. Om du fortfarande väger den modellen mot vanliga lösenord täcker guiden om AES-256-lösenordskryptering och behörighetsflaggor den andra sidan av den avvägningen
Skriva RSA-mottagare med EnablePubKeyEncryption
För RSA-certifikat anropar du EnablePubKeyEncryption med aes256 och sedan AddPubKeyRecipientCertificate en gång per DER-kodat certifikat före BeginDoc. Hjälpfunktionen bygger ett RSAES-OAEP-kuvert i processen med THPDFRSAOAEPHash-värden för OAEP-digesten och MGF1-digesten (rohSHA256, rohSHA384 eller rohSHA512), och krypterar kuvertinnehållet med 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); // exakt 20 byte, även för AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // standardnyckeltypen är aes128
// Granskare A får skriva ut; granskare B får bara läsa och extrahera
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;
Tre detaljer i den listningen bär hela vikten. För det första är frölängden låst till 20 byte för varje nyckeltyp, AES-256 inkluderat; EnablePubKeyEncryption kastar vid varje annan längd. För det andra är standardvärdet för EnablePubKeyEncryption aes128, och båda certifikathjälpfunktionerna vägrar köra om inte nyckeltypen är aes256, så att glömma det andra argumentet ger dig undantaget "certificate envelopes require aes256". De äldre chifrren (k40, k128, aes128) fungerar fortfarande, men bara genom AddPubKeyRecipient med ett kuvert du byggt någon annanstans. För det tredje är AES-256 public-key-kryptering en PDF 2.0-funktion, så HotPDF höjer dokumentversionen till 2.0 automatiskt. Med StrictVersionLock satt på en lägre version returnerar EnablePubKeyEncryption utan att aktivera något, och felet dyker först upp på nästa rad som "call EnablePubKeyEncryption first". Att byta kryptering under en inkrementell uppdatering kastar EInvalidOpException direkt
Lägga till ECDH-mottagare: P-256, P-384, P-521, X25519 och X448
För elliptiska kurvcertifikat skriver AddPubKeyAgreementRecipientWithSecret en CMS key-agreement-mottagare (KeyAgreeRecipientInfo, KARI-strukturen från RFC 5753, med X25519- och X448-profilen från RFC 8418) och beräknar den delade ECDH-hemligheten i processen. Du väljer kurvan med ett THPDFPubKeyAgreementScheme-värde: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 eller pkasX448. Schemat måste matcha nyckeln i certifikatet, annars kastar anropet "Certificate key does not match the requested agreement scheme". Under huven får varje kuvert en färsk slumpmässig UKM på 32 byte, en nyckelkrypteringsnyckel härledd med stdDH-KDF:n (SHA-256 för P-256 och X25519, SHA-384 för P-384, SHA-512 för P-521 och X448) och en AES-256 key wrap enligt RFC 3394. Den delade hemligheten i sig kommer från ren Pascal-kurvkod, utan någon plattformskryptoprovider inblandad; artikeln om ren Pascal NIST-kurvaritmetik förklarar hur det lagret byggdes och verifierades. För Montgomery-kurvorna kan hela det efemära nyckelparet genereras lokalt:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Färsk efemär skalär per kuvert; klampning sker inne i stegen
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: endast meningsfull för NIST-kurvorna
finally
HPDFSecureClearBytes(Scalar);
end;
end;
NIST-kurvorna kräver mer av anroparen. HotPDF levererar public-key-hjälpare bara för X25519 och X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), så för P-256, P-384 och P-521 genererar du det efemära nyckelparet med egna verktyg och skickar en big-endian-skalär med exakt fältstorleken (32, 48 eller 66 byte) plus den matchande okomprimerade punkten 0x04||X||Y som OriginatorPublicKey. HotPDF validerar mottagarpunkten mot kurvekvationen, men den kan inte kontrollera att din originator publika nyckel faktiskt hör till din skalär. Omaka halvor ger ändå ett helt välbildat kuvert som ingen mottagare kan öppna, vilket är varför en rundtur med inläsning hör hemma i din testsvit, inte bara en filstorlekskontroll
Varför spelar /Recipients-ordningen roll?
Ordningen på /Recipients spelar roll för att filnyckeln är en digest över fröet och varje kuvert i arrayordning, så skrivare och läsare måste hasha samma byte i samma sekvens. HotPDF behåller kuverten i den ordning du lägger till dem och skriver dem oförändrade, vilket betyder att du kan lägga till mottagare i vilken ordning du vill, men ingenting nedströms får ordna om, koda om eller "städa upp" den arrayen. De flesta av de riktiga buggarna på det här området var en variation på det temat, där två sidor hashade något olika byte:
- Att lagra dynamiska arrayer i en
TListviaAddbehåller bara en rå pekare medan referensräknaren stannar hos den lokala variabeln. NästaSetLengthfrigör bufferten och kan återanvända den, så varje plats slutade aliasa sista kuvertet och filer med flera mottagare härledde fel nyckel. Fixen är att lagra en ägd kopia medList.Add(Pointer(System.Copy(Bytes))) - Kuvertuppackning tolkar DER:en på plats, och nyckelåtervinningsgenomgången hashade ursprungligen samma levande arrayer. Läsaren tar nu ögonblicksbilder av rena kopior av varje kuvert innan någon uppackning rör dem, och digesten löper över ögonblicksbilderna
- Binär DER som passerar en Unicode-
TStringListfår byte på eller över$80omkodade av kodsidan, så HotPDF lagrar kuverten som hex-text internt - Krypterade och binära strängar måste skrivas som hex-strängar. En literal sträng är underkastad radslutnormalisering, där CR, LF och CRLF alla blir en enda LF (ISO 32000-1 §7.3.4.2), och det skriver om chiffertexten tyst. HotPDF skickar ut varje
/Recipients-post som en hex-sträng och undantar den från strängkryptering, eftersom varje läsare behöver kuverten innan den håller någon nyckel - Första byten i en DER
BIT STRINGräknar oanvända bitar och måste vara noll för byte-justerade nycklar. Att lämna den oinitierad efterSetLengthskrev vad som helst som låg på stacken, och en strikt uppackare avvisade originatornyckeln, så en fil kunde ibland vägra öppnas med just den nyckel den skrivits för - När samma nyckel ändå inte kan dekryptera, jämför lager för lager: filnyckeln, sedan chiffertext-prefixet (IV:en), sedan objektnyckeln, sedan klartexten. Buggen bor precis efter det första lagret som inte stämmer
Hur öppnar du en certifikatkrypterad PDF med en privat nyckel?
För att öppna en certifikatkrypterad PDF registrerar du det privata nyckelmaterialet innan du anropar LoadFromFile, eftersom HotPDF återvinner filnyckeln under det strukturella passet. Tilldela en RSA- eller EC-nyckel tolkad med HPDFParsePFX till PubSecKeyMaterial, lägg till fler RSA-nycklar med AddPubSecKeyMaterial och registrera råa ECDH-skalärer med AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), med konstanterna HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 eller HPDFOIDECP521. NIST-kurvorna kräver mottagarens egen okomprimerade publika punkt; Montgomery-kurvorna ignorerar den
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);
// Valfritt: plocka kuvertet direkt i stället för att prova alla
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = prova alla kuvert i ordning
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Utan callback provar HotPDF varje kuvert mot varje registrerad nyckel: primärnyckeln först, sedan varje extra RSA-nyckel, sedan EC-materialet. PubSecRecipientQuery tar emot kuvertantalet och returnerar ett 0-baserat index eller -1, och ett index utanför arrayen kastar ett undantag i stället för att klämmas. Observera att AddPubSecKeyMaterial bara accepterar RSA-material (den kräver en modul och en privat exponent), så EC-nycklar hör hemma i PubSecKeyMaterial eller AddPubSecAgreementKeyMaterial. När ingen nyckel vecklar upp något kuvert returnerar återvinningssteget utan filnyckel i stället för att kasta, så kontrollera att innehållet du förväntar dig faktiskt dekrypterades i stället för att lita på att inläsningsanropet returnerade
Vad HotPDF inte garanterar
HotPDF garanterar att dess egen skrivare och läsare är överens byte för byte, och den bygger kuvert som följer de CMS-strukturer som citerats ovan. Den garanterar inte att varje PDF-viewer öppnar varje kombination. Stödet för RSA-OAEP key transport och för X25519- eller X448-mottagare varierar mellan läsare och versioner, och vi har inte publicerat kompatibilitetsresultat för de kombinationerna. Om ett dokument måste öppnas i en specifik viewer, kryptera en testfil för ett testcertifikat av samma nyckeltyp och öppna den där innan du binder dig för ett schema. Behörigheterna som bärs i kuvertet förblir policy som konform programvara hedrar, exakt som under lösenordskryptering. Frökvaliteten är också ditt ansvar: AESGenerateRandomBytes finns för det jobbet, och HotPDF raderar sin kopia av fröet så snart filnyckeln härletts. Om du också behöver en sträng, ström eller bilaga som använder ett annat crypt-filter visar policyguiden för crypt-filtren StmF, StrF och EFF vilka filternamn public-key-handlern accepterar
Certifikatkryptering, RSA-OAEP- och ECDH-mottagarkuvert och inläsning av privat nyckel kommer alla i HotPDF Delphi PDF-komponenten, jämte lösenordskryptering, digitala signaturer och resten av ISO 32000-verktygslådan för Delphi och C++Builder