HotPDF szyfruje PDF dla konkretnych posiadaczy certyfikatów przez procedurę bezpieczeństwa klucza publicznego z ISO 32000: EnablePubKeyEncryption bierze 20-bajtowe losowe ziarno, a każdy odbiorca dostaje własną kopertę CMS, budowaną przez AddPubKeyRecipientCertificate dla kluczy RSA (transport klucza RSA-OAEP) albo AddPubKeyAgreementRecipientWithSecret dla kluczy krzywych eliptycznych (ECDH na P-256, P-384, P-521, X25519 albo X448). Nikt nie dzieli się hasłem; kto trzyma pasujący klucz prywatny, ten otwiera plik
Przypadek użycia to zawsze jakaś wersja tej samej historii. Kwartalny pakiet audytowy idzie do trzech zewnętrznych recenzentów, prawny chce, żeby każdy go przeczytał, tylko jeden z nich może go drukować, a nikt nie chce hasła siedzącego w wątku mailowym obok załącznika. Szyfrowanie hasłem tego nie wyrazi. Szyfrowanie certyfikatami potrafi, bo każdy odbiorca otwiera dokument kluczem, który już trzyma, i każdy odbiorca może nieść inny zestaw uprawnień wewnątrz własnej koperty
Czym szyfrowanie PDF certyfikatami różni się od hasła?
Szyfrowany kluczem publicznym PDF wyprowadza klucz pliku z losowego ziarna plus dokładnych bajtów każdej koperty odbiorcy, a nie z czegokolwiek, co człowiek wpisze. Procedura jest opisana w ISO 32000-1 §7.6.4 (§7.6.5 w ISO 32000-2), a koperty to struktury CMS EnvelopedData z definicji RFC 5652. HotPDF pisze /Filter /Adobe.PubSec z /SubFilter /adbe.pkcs7.s5; dla AES-256 znaczy to /V 5 i wpis /DefaultCryptFilter pod /CF z /CFM /AESV3, a tablica /Recipients mieszka wewnątrz tego filtra kryptograficznego. Każda koperta szyfruje 24 bajty: 20-bajtowe ziarno, po którym idzie 32-bitowe słowo uprawnień tamtego odbiorcy. Wartość /P w słowniku szyfrowania to tylko miejsce na znak, bo prawdziwe uprawnienia podróżują wewnątrz każdej koperty. Przy wczytywaniu czytnik odpina jedną kopertę, odzyskuje ziarno i haszuje ziarno razem z każdą kopertą w kolejności /Recipients (SHA-256 dla AES-256, SHA-1 dla starszych szyfrów), żeby odbudować klucz pliku. Jeśli wciąż rozstrzygasz między tym modelem a zwykłymi hasłami, przewodnik po szyfrowaniu hasłem AES-256 i flagach uprawnień pokazuje drugą stronę tego kompromisu
Zapisywanie odbiorców RSA przez EnablePubKeyEncryption
Dla certyfikatów RSA wołaj EnablePubKeyEncryption z aes256, a potem AddPubKeyRecipientCertificate raz na każdy certyfikat w kodowaniu DER przed BeginDoc. Helper buduje kopertę RSAES-OAEP w procesie z wartościami THPDFRSAOAEPHash dla skrótu OAEP i skrótu MGF1 (rohSHA256, rohSHA384 albo rohSHA512) i szyfruje zawartość koperty 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); // dokładnie 20 bajtów, nawet dla AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // domyślny typ klucza to aes128
// Recenzent A może drukować; recenzent B może tylko czytać i wyodrębniać
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;
Trzy szczegóły w tym listingu trzymają ciężar. Po pierwsze, długość ziarna jest na sztywno 20 bajtów dla każdego typu klucza, AES-256 włącznie; EnablePubKeyEncryption rzuca przy każdej innej długości. Po drugie, EnablePubKeyEncryption ma domyślnie aes128, a oba helpery certyfikatowe odmawiają biegu, dopóki typ klucza to nie aes256, więc zapomnienie drugiego argumentu kupuje ci wyjątek „certificate envelopes require aes256”. Starsze szyfry (k40, k128, aes128) nadal działają, ale tylko przez AddPubKeyRecipient z kopertą zbudowaną gdzie indziej. Po trzecie, szyfrowanie kluczem publicznym AES-256 to cecha PDF 2.0, więc HotPDF podnosi wersję dokumentu do 2.0 automatycznie. Z ustawionym StrictVersionLock na niższej wersji EnablePubKeyEncryption wraca bez włączania czegokolwiek, a porażka wychodzi dopiero w następnym wierszu jako „call EnablePubKeyEncryption first”. Zmiana szyfrowania w trakcie aktualizacji przyrostowej rzuca EInvalidOpException od razu
Dodawanie odbiorców ECDH: P-256, P-384, P-521, X25519 i X448
Dla certyfikatów krzywych eliptycznych AddPubKeyAgreementRecipientWithSecret pisze odbiorcę uzgadniania klucza CMS (KeyAgreeRecipientInfo, struktura KARI z RFC 5753, z profilem X25519 i X448 z RFC 8418) i liczy wspólny sekret ECDH w procesie. Krzywą wybierasz wartością THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 albo pkasX448. Schemat musi pasować do klucza w certyfikacie, inaczej wywołanie rzuca „Certificate key does not match the requested agreement scheme”. Pod spodem każda koperta dostaje świeże losowe 32-bajtowe UKM, klucz szyfrujący klucz wyprowadzony KDF-em stdDH (SHA-256 dla P-256 i X25519, SHA-384 dla P-384, SHA-512 dla P-521 i X448) oraz zawinięcie klucza AES-256 z definicji RFC 3394. Sam wspólny sekret wychodzi z czystego kodu Pascal krzywych, bez angażowania platformowego dostawcy kryptografii; artykuł o arytmetyce krzywych NIST w czystym Pascalu wyjaśnia, jak ta warstwa powstała i została zweryfikowana. Dla krzywych Montgomery cały efemeryczny klucz da się wygenerować lokalnie:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Świeży efemeryczny skalar na kopertę; przycinanie dzieje się w drabince
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: ma znaczenie tylko dla krzywych NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Krzywe NIST wymagają od wołającego więcej. HotPDF dowozi helpery klucza publicznego tylko dla X25519 i X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), więc dla P-256, P-384 i P-521 generujesz efemeryczną parę kluczy własnym narzędziem i podajesz skalar big-endian o dokładnie rozmiarze ciała (32, 48 albo 66 bajtów) plus pasujący nieskompresowany punkt 0x04||X||Y jako OriginatorPublicKey. HotPDF waliduje punkt odbiorcy względem równania krzywej, ale nie umie sprawdzić, czy twój klucz publiczny inicjatora faktycznie należy do twojego skalara. Niedopasowane połówki i tak wyprodukują idealnie poprawną kopertę, której żaden odbiorca nie otworzy, i dlatego wczytanie w obie strony należy do twojego zestawu testów, a nie tylko sprawdzenie rozmiaru pliku
Dlaczego kolejność /Recipients ma znaczenie?
Kolejność /Recipients ma znaczenie, bo klucz pliku to skrót z ziarna i każdej koperty w kolejności tablicy, więc zapisujący i czytający muszą haszować te same bajty w tej samej sekwencji. HotPDF trzyma koperty w kolejności dodawania i zapisuje je bez zmian, co znaczy, że odbiorców możesz dodawać w dowolnej kolejności, ale nic downstream nie wolno tej tablicy przestawiać, przekodowywać ani „porządkować”. Większość prawdziwych błędów w tym rejonie to była wariacja na ten temat, gdzie dwie strony haszowały minimalnie różne bajty:
- Trzymanie tablic dynamicznych w
TListprzezAddzostawia tylko surowy wskaźnik, podczas gdy licznik referencji zostaje przy zmiennej lokalnej. NastępneSetLengthzwalnia bufor i może go ponownie użyć, więc każde gniazdo kończyło się aliasem ostatniej koperty, a pliki wielu odbiorców wyprowadzały zły klucz. Poprawka to przechowywanie posiadanej kopii przezList.Add(Pointer(System.Copy(Bytes))) - Odpinanie kopert parsuje DER w miejscu, a przebieg odzyskiwania klucza haszował pierwotnie te same żywe tablice. Czytnik robi teraz migawki czystych kopii każdej koperty, zanim jakiekolwiek odpinanie je dotknie, a skrót biegnie po migawkach
- Binarny DER przepuszczony przez Unicode
TStringListdostaje bajty od$80wzżej przekodowane przez stronę kodową, więc HotPDF trzyma koperty wewnętrznie jako tekst szesnastkowy - Szyfrowane i binarne łańcuchy muszą być zapisywane jako łańcuchy szesnastkowe. Łańcuch literalny podlega normalizacji końców wiersza, gdzie CR, LF i CRLF stają się jednym LF (ISO 32000-1 §7.3.4.2), i to po cichu przepisuje szyfrogram. HotPDF emituje każdy wpis
/Recipientsjako łańcuch szesnastkowy i zwalnia go z szyfrowania łańcuchów, bo każdy czytnik potrzebuje kopert, zanim trzyma jakikolwiek klucz - Pierwszy bajt DER
BIT STRINGliczy nieużywane bity i musi być zerem dla kluczy wyrównanych do bajta. Pozostawienie go niezainicjalizowanym poSetLengthzapisywało, co leżało na stosie, a restrykcyjny odpinacz odrzucał klucz inicjatora, więc plik mógł od czasu do czasu nie otworzyć się kluczem, pod który został zapisany - Gdy ten sam klucz nadal nie umie odszyfrować, porównuj warstwa po warstwie: klucz pliku, potem prefiks szyfrogramu (IV), potem klucz obiektu, potem plaintext. Błąd mieszka zaraz za pierwszą warstwą, która się nie zgadza
Jak otworzyć zaszyfrowany certyfikatem PDF kluczem prywatnym?
Żeby otworzyć PDF zaszyfrowany certyfikatami, zarejestruj materiał klucza prywatnego przed wołaniem LoadFromFile, bo HotPDF odzyskuje klucz pliku w przebiegu strukturalnym. Przypisz klucz RSA albo EC sparsowany przez HPDFParsePFX do PubSecKeyMaterial, dodaj dalsze klucze RSA przez AddPubSecKeyMaterial, a surowe skalary ECDH zarejestruj przez AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), używając stałych HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 albo HPDFOIDECP521. Krzywe NIST wymagają własnego nieskompresowanego punktu publicznego odbiorcy; krzywe Montgomery go 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);
// Opcjonalnie: wybierz kopertę wprost, zamiast próbować wszystkich
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = próbuj każdą kopertę po kolei
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Bez callbacka HotPDF próbuje każdej koperty względem każdego zarejestrowanego klucza: najpierw klucza głównego, potem każdego dodatkowego klucza RSA, potem materiału EC. PubSecRecipientQuery dostaje liczbę kopert i zwraca indeks liczony od zera albo -1, a indeks poza tablicą rzuca wyjątek, zamiast być przyciętym. Uwaga: AddPubSecKeyMaterial przyjmuje wyłącznie materiał RSA (upiera się przy modulusie i prywatnym wykładniku), więc klucze EC należą do PubSecKeyMaterial albo AddPubSecAgreementKeyMaterial. Gdy żaden klucz nie odpina żadnej koperty, krok odzyskiwania wraca bez klucza pliku zamiast rzucać, więc zweryfikuj, że oczekiwana zawartość faktycznie się odszyfrowała, zamiast ufać, że wywołanie wczytania wróciło
Czego HotPDF nie gwarantuje
HotPDF gwarantuje, że jego własny zapis i odczyt zgadzają się bajt w bajt, i buduje koperty zgodne z przywołanymi wyżej strukturami CMS. Nie gwarantuje, że każda przeglądarka PDF otworzy każdą kombinację. Wsparcie transportu klucza RSA-OAEP i odbiorców X25519 albo X448 różni się między czytnikami i wersjami, a my nie publikowaliśmy wyników zgodności dla tych kombinacji. Jeśli dokument musi otworzyć się w konkretnej przeglądarce, zaszyfruj plik testowy certyfikatem testowym tego samego typu klucza i otwórz go tam, zanim zobowiążesz się do schematu. Uprawnienia niesione w kopercie pozostają polityką, którą zgodne oprogramowanie respektuje, dokładnie tak samo jak przy szyfrowaniu hasłem. Jakość ziarna też jest twoją odpowiedzialnością: AESGenerateRandomBytes jest po to, a HotPDF wyciera swoją kopię ziarna, gdy tylko klucz pliku został wyprowadzony. Jeśli potrzebujesz też, żeby łańcuch, strumień albo załącznik używał innego filtra kryptograficznego, przewodnik po politykach filtrów kryptograficznych dla StmF, StrF i EFF pokazuje, jakie nazwy filtrów przyjmuje procedura klucza publicznego
Szyfrowanie certyfikatami, koperty odbiorców RSA-OAEP i ECDH oraz wczytywanie kluczy prywatnych trafiają razem do komponentu PDF HotPDF dla Delphi, obok szyfrowania hasłem, podpisów cyfrowych i reszty zestawu narzędzi ISO 32000 dla Delphi i C++Buildera