O HotPDF criptografa um PDF para detentores de certificado específicos por meio do security handler de chave pública da ISO 32000: o EnablePubKeyEncryption recebe uma seed aleatória de 20 bytes, e cada recipient ganha o envelope CMS próprio dele, montado pelo AddPubKeyRecipientCertificate para chaves RSA (key transport RSA-OAEP) ou pelo AddPubKeyAgreementRecipientWithSecret para chaves de curva elíptica (ECDH em P-256, P-384, P-521, X25519 ou X448). Ninguém compartilha senha; quem tiver a chave privada correspondente abre o arquivo
O caso de uso é sempre alguma versão da mesma história. Um pacote de auditoria trimestral vai para três revisores externos, o jurídico quer que cada um leia, só um deles pode imprimir, e ninguém quer uma senha parada num thread de e-mail ao lado do anexo. Criptografia por senha não expressa isso. Criptografia por certificado expressa, porque cada recipient destrava o documento com uma chave que já tem, e cada recipient pode carregar um conjunto de permissões diferente dentro do envelope dele
Como a criptografia de PDF por certificado difere de uma senha?
Um PDF criptografado por chave pública deriva a file key dele de uma seed aleatória mais os bytes exatos de todo envelope de recipient, não de nada que uma pessoa digite. O handler é descrito na ISO 32000-1 §7.6.4 (§7.6.5 na ISO 32000-2), e os envelopes são estruturas CMS EnvelopedData conforme a RFC 5652. O HotPDF escreve /Filter /Adobe.PubSec com /SubFilter /adbe.pkcs7.s5; para AES-256 isso significa /V 5 e uma entrada /DefaultCryptFilter sob /CF com /CFM /AESV3, e o array /Recipients mora dentro desse crypt filter. Cada envelope criptografa 24 bytes: a seed de 20 bytes seguida da palavra de permissão de 32 bits daquele recipient. O valor /P no dicionário de criptografia é só um placeholder, porque as permissões de verdade viajam dentro de cada envelope. No load, um reader abre um envelope, recupera a seed, e faz o hash da seed junto com todo envelope na ordem do /Recipients (SHA-256 para AES-256, SHA-1 para as cifras mais antigas) para reconstruir a file key. Se você ainda está decidindo entre esse modelo e senhas comuns, o guia de criptografia por senha AES-256 e flags de permissão cobre o outro lado desse trade-off
Escrevendo recipients RSA com EnablePubKeyEncryption
Para certificados RSA, chame o EnablePubKeyEncryption com aes256 e depois o AddPubKeyRecipientCertificate uma vez por certificado codificado em DER antes do BeginDoc. O helper monta um envelope RSAES-OAEP in-process com valores THPDFRSAOAEPHash para o digest OAEP e o digest MGF1 (rohSHA256, rohSHA384 ou rohSHA512), e criptografa o conteúdo do envelope com 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); // exatamente 20 bytes, mesmo para AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // o key type default é aes128
// O revisor A pode imprimir; o revisor B só pode ler e extrair
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;
Três detalhes dessa listagem sustentam tudo. Primeiro, o comprimento da seed é fixo em 20 bytes para todo key type, AES-256 incluído; o EnablePubKeyEncryption levanta exceção com qualquer outro comprimento. Segundo, o EnablePubKeyEncryption tem default aes128, e ambos os helpers de certificado se recusam a rodar a menos que o key type seja aes256, então esquecer o segundo argumento te dá a exceção "certificate envelopes require aes256". As cifras legadas (k40, k128, aes128) ainda funcionam, mas só via AddPubKeyRecipient com um envelope que você montou em outro lugar. Terceiro, criptografia de chave pública AES-256 é um recurso do PDF 2.0, então o HotPDF sobe a versão do documento para 2.0 automaticamente. Com StrictVersionLock setado numa versão menor, o EnablePubKeyEncryption retorna sem habilitar nada, e a falha só aparece na linha seguinte como "call EnablePubKeyEncryption first". Trocar a criptografia durante um update incremental levanta EInvalidOpException de cara
Adicionando recipients ECDH: P-256, P-384, P-521, X25519 e X448
Para certificados de curva elíptica, o AddPubKeyAgreementRecipientWithSecret escreve um recipient de key agreement CMS (KeyAgreeRecipientInfo, a estrutura KARI da RFC 5753, com o perfil X25519 e X448 da RFC 8418) e calcula o segredo compartilhado ECDH in-process. Você escolhe a curva com um valor THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ou pkasX448. O scheme tem que casar com a chave do certificado, ou a chamada levanta "Certificate key does not match the requested agreement scheme". Por baixo dos panos, cada envelope ganha um UKM aleatório fresco de 32 bytes, uma key-encryption key derivada com o KDF stdDH (SHA-256 para P-256 e X25519, SHA-384 para P-384, SHA-512 para P-521 e X448), e um key wrap AES-256 conforme a RFC 3394. O segredo compartilhado em si vem de código de curvas em Pascal puro, sem provedor de criptografia de plataforma envolvido; o artigo sobre aritmética de curvas NIST em Pascal puro explica como essa camada foi construída e verificada. Para as curvas de Montgomery, o par de chaves efêmero inteiro pode ser gerado localmente:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Scalar efêmero fresco por envelope; o clamping acontece dentro da ladder
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: só tem significado para as curvas NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
As curvas NIST pedem mais de quem chama. O HotPDF distribui helpers de chave pública só para X25519 e X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), então para P-256, P-384 e P-521 você gera o par de chaves efêmero com a toolchain própria e passa um scalar big-endian exatamente do tamanho do campo (32, 48 ou 66 bytes) mais o ponto não comprimido 0x04||X||Y correspondente como OriginatorPublicKey. O HotPDF valida o ponto do recipient contra a equação da curva, mas não consegue checar que a sua chave pública de originator realmente pertence ao seu scalar. Metades que não combinam ainda produzem um envelope perfeitamente bem-formado que nenhum recipient consegue abrir, e é por isso que um load de round trip pertence à sua suíte de testes, não só uma checagem de tamanho de arquivo
Por que a ordem do /Recipients importa?
A ordem do /Recipients importa porque a file key é um digest sobre a seed e todo envelope na ordem do array, então writer e reader precisam fazer o hash dos mesmos bytes na mesma sequência. O HotPDF mantém os envelopes na ordem em que você os adiciona e os escreve inalterados, o que significa que você pode adicionar recipients em qualquer ordem que quiser, mas nada a jusante pode reordenar, recodificar ou "limpar" esse array. A maioria dos bugs reais nessa área foi uma variação desse tema, em que os dois lados fizeram o hash de bytes ligeiramente diferentes:
- Guardar arrays dinâmicos num
TListviaAddmantém só um ponteiro cru enquanto o reference count fica com a variável local. O próximoSetLengthlibera o buffer e pode reutilizá-lo, então todo slot acabava fazendo aliasing do último envelope e arquivos multi-recipient derivavam a chave errada. A correção é guardar uma cópia própria comList.Add(Pointer(System.Copy(Bytes))) - A abertura de envelopes parseia o DER in place, e a passagem de recuperação de chave originalmente fazia o hash desses mesmos arrays vivos. O reader agora tira snapshots de cópias íntegras de todo envelope antes de qualquer unwrap tocá-los, e o digest roda sobre os snapshots
- DER binário passado por um
TStringListUnicode tem bytes em$80ou acima recodificados pela code page, então o HotPDF guarda os envelopes como texto hex internamente - Strings criptografadas e binárias precisam ser escritas como hex strings. Uma literal string está sujeita à normalização de fim de linha, em que CR, LF e CRLF viram todos um único LF (ISO 32000-1 §7.3.4.2), e isso reescreve o ciphertext em silêncio. O HotPDF emite toda entrada de
/Recipientscomo hex string e a isenta da criptografia de strings, já que todo reader precisa dos envelopes antes de ter qualquer chave - O primeiro byte de um
BIT STRINGDER conta bits não usados e precisa ser zero para chaves alinhadas a byte. Deixá-lo sem inicializar depois doSetLengthescrevia o que quer que estivesse na stack, e um unwrapper estrito rejeitava a chave do originator, então um arquivo podia ocasionalmente falhar ao abrir com a própria chave para a qual foi escrito - Quando a mesma chave ainda não consegue decifrar, compare camada por camada: a file key, depois o prefixo do ciphertext (o IV), depois a object key, depois o plaintext. O bug mora logo depois da primeira camada que discorda
Como abrir um PDF criptografado por certificado com uma chave privada?
Para abrir um PDF criptografado por certificado, registre o material de chave privada antes de chamar o LoadFromFile, porque o HotPDF recupera a file key durante a passagem estrutural. Atribua uma chave RSA ou EC parseada com HPDFParsePFX ao PubSecKeyMaterial, adicione outras chaves RSA com AddPubSecKeyMaterial, e registre scalars ECDH crus com AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), usando as constantes HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ou HPDFOIDECP521. As curvas NIST exigem o ponto público não comprimido do próprio recipient; as curvas de Montgomery o ignoram
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);
// Opcional: escolhe o envelope direto em vez de tentar todos
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = tenta todo envelope em ordem
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Sem callback, o HotPDF tenta todo envelope contra toda chave registrada: a chave primária primeiro, depois cada chave RSA adicional, depois o material EC. O PubSecRecipientQuery recebe a contagem de envelopes e retorna um índice zero-based ou -1, e um índice fora do array levanta exceção em vez de ser clampado. Note que o AddPubSecKeyMaterial aceita só material RSA (ele exige um modulus e um expoente privado), então chaves EC pertencem ao PubSecKeyMaterial ou ao AddPubSecAgreementKeyMaterial. Quando nenhuma chave abre nenhum envelope, o passo de recuperação retorna sem file key em vez de levantar exceção, então verifique que o conteúdo que você espera realmente foi decifrado em vez de confiar que a chamada de load retornou
O que o HotPDF não garante
O HotPDF garante que o writer e o reader dele concordam byte a byte, e monta envelopes que seguem as estruturas CMS citadas acima. Ele não garante que todo viewer de PDF abra toda combinação. O suporte a key transport RSA-OAEP e a recipients X25519 ou X448 varia entre readers e versões, e nós não publicamos resultados de compatibilidade para essas combinações. Se um documento precisa abrir num viewer específico, criptografe um arquivo de teste para um certificado de teste do mesmo key type e abra-o lá antes de se comprometer com um scheme. As permissões carregadas no envelope continuam sendo política que software em conformidade honra, exatamente como sob criptografia por senha. A qualidade da seed também é sua responsabilidade: o AESGenerateRandomBytes existe para esse trabalho, e o HotPDF apaga a cópia dele da seed assim que a file key é derivada. Se você também precisa que uma string, stream ou anexo use um crypt filter diferente, o guia de políticas de crypt filter para StmF, StrF e EFF mostra quais nomes de filtro o handler de chave pública aceita
Criptografia por certificado, envelopes de recipient RSA-OAEP e ECDH, e carregamento de chave privada entram todos no componente HotPDF PDF para Delphi, junto com criptografia por senha, assinaturas digitais e o resto do toolset ISO 32000 para Delphi e C++Builder