Gerar material de chave de criptografia PDF com a função Random da RTL é uma armadilha no Free Pascal: a menos que algo chame Randomize, todo processo produz a mesma sequência de bytes. builds no Linux e no macOS gravariam chaves de criptografia de arquivo idênticas, salts, CBC IVs e prefixos de nonce AES-GCM iguais a cada execução. O PDFiumPas lê /dev/urandom em alvos não Windows em vez disso e dispara uma exceção quando não consegue
A armadilha em si cabe em um loop de quatro linhas. A lição mais útil é por que uma suíte de testes de criptografia, com AESV3 e AESV4, com e sem PDF MAC, pode ficar verde bem por cima dela. Aleatoriedade constante por processo é invisível para todo teste que roda dentro de um processo, e é exatamente assim que testes de criptografia costumam ser escritos
Onde o PDFiumPas precisa de bytes aleatórios?
Todo byte aleatório na pilha de criptografia do PDFiumPas vem de uma única procedure, a AesGenerateRandomBytes na unit FPdfAes, então uma fonte ruim contamina tudo. O security handler padrão na ISO 32000-2 §7.6.4 e a extensão AESV4 na ISO/TS 32003 consomem esses bytes nestes lugares:
- A chave de criptografia de arquivo de 32 bytes, gerada nova pelo DeriveEncryptionKeys para cada documento e depois embrulhada em /UE e /OE sob chaves derivadas da senha
- Dois salts de 16 bytes, um guardado nos últimos 16 bytes de /U e um nos últimos 16 bytes de /O, cada um dividido num salt de validação de 8 bytes e num key salt de 8 bytes
- Os bytes 12 a 15 do plaintext por trás de /Perms, que a ISO 32000-2 preenche com dados aleatórios antes de o bloco ser criptografado sob a chave de arquivo
- Um CBC IV de 16 bytes prefixado a toda string e stream criptografados num documento AESV3
- Um prefixo de nonce de 8 bytes para documentos AESV4, seguido de um contador por objeto de 4 bytes que começa em zero
- O /KDFSalt de 32 bytes e a chave MAC quando EnableIntegrityProtection está setado
Por que todo processo produziria a mesma chave?
O gerador pseudo-aleatório da RTL parte de RandSeed = 0 a menos que o programa chame Randomize, então um ramo não Windows que preenche o buffer de chaves com ele recebe a mesma sequência em todo processo. Um comentário dizendo que o gerador é semeado a partir do GetTickCount64 não ajuda quando nenhuma linha de código realmente o faz; o comentário vira o único lugar em que a semente existe:
// A armadilha: um gerador da RTL sem semente como fonte de chaves
// (um comentário prometendo uma semente GetTickCount64 não aplica uma)
P := PByte(Buffer);
for I := 0 to Count - 1 do
P[I] := Byte(Random(256));
A sequência recomeça a cada processo e avança dentro dele, então o primeiro documento que qualquer processo criptografa compartilha a chave de arquivo dele com o primeiro documento de todo outro processo rodando a mesma build, o segundo com o segundo, e assim por diante. A chave de arquivo em R5, R6 e R7 não depende da senha de forma alguma, já que a senha só a embrulha, o que significa que qualquer um capaz de reproduzir a sequência segura a chave sem saber senha nenhuma. O AESV4 adiciona uma segunda falha: a mesma chave com o mesmo prefixo de 8 bytes e um contador recomeçando em zero repete nonces GCM, o que a NIST SP 800-38D §8 proíbe terminantemente. Um nonce GCM repetido sob uma chave revela o XOR dos dois plaintexts e expõe a subchave de autenticação, então as tags de que a criptografia AESV4-GCM e o token PDF MAC dependem param de significar algo. Confidencialidade e integridade vão embora juntas
O escopo é mais estreito do que aquele parágrafo pode sugerir. No Windows a pergunta nem surge, porque o ramo Windows chama CryptGenRandom pela advapi32 com CRYPT_VERIFYCONTEXT e dispara erro quando isso falha. A armadilha é específica de alvos não Windows, o que na prática significa aplicações Lazarus e Free Pascal no Linux e no macOS, mais uma entrada para a lista de armadilhas de Delphi versus FPC em builds do PDFium
Por que Randomize não é a resposta?
Chamar Randomize esconde o sintoma sem corrigir a fonte, porque RandSeed é um valor de 32 bits e o Randomize o deriva do relógio. Isso limita o número de streams de chave possíveis a 2^32, e saber mais ou menos quando um arquivo foi escrito corta a busca bem abaixo disso, o que não é nada perto de uma chave AES de 256 bits. Material de chave precisa vir do pool de entropia do kernel, então a AesGenerateRandomBytes lê /dev/urandom, itera sobre leituras curtas, e dispara erro se o pool não entregar todo byte pedido:
Handle := FileOpen('/dev/urandom', fmOpenRead or fmShareDenyNone);
if Handle <> THandle(-1) then
try
Remaining := Count;
while Remaining > 0 do
begin
Got := FileRead(Handle, P^, Remaining);
if Got <= 0 then
Break; // falha ou fim inesperado do stream
Inc(P, Got);
Dec(Remaining, Got);
end;
if Remaining = 0 then
Exit;
finally
FileClose(Handle);
end;
raise Exception.Create('FPdfAes: /dev/urandom unavailable; refusing to emit predictable key/IV');
Recusar é deliberado, e bate com o que o ramo Windows faz quando o CryptGenRandom não está disponível. Um save criptografado que falha é um incidente que você nota no mesmo dia; um save bem-sucedido com chaves previsíveis é um que você fica sabendo pelos outros. Duas consequências práticas decorrem. Um container mínimo ou chroot sem um /dev populado falha na criptografia em vez de degradar em silêncio, então monte-o. E como a exceção se propaga para fora do TPdf.SaveAsEncrypted depois de o arquivo alvo ter sido aberto com fmCreate, um arquivo de saída vazio fica para trás para o seu error handler apagar
Por que os testes de round-trip não pegam isso?
Um teste de round-trip não consegue ver aleatoriedade constante, porque a descriptografia recupera qualquer chave de arquivo que a criptografia tenha escolhido. O teste criptografa um documento, abre de novo com a senha, desembrulha a chave de /UE, e descriptografa todo objeto; uma chave previsível desembrulha e descriptografa exatamente tão bem quanto uma aleatória, e as tags GCM verificam porque foram computadas com essa mesma chave. Até um teste que criptografa duas vezes e assevera que as duas saídas diferem passa, já que a segunda chamada no mesmo processo puxa os próximos bytes da sequência. A propriedade que importa, uma chave diferente em todo processo, só é observável comparando a saída entre processos. Sempre que o mesmo código produz e consome um valor, os testes ficam cegos para classes inteiras de defeito, e aleatoriedade é o exemplo mais puro
Como testar a aleatoriedade de chaves entre processos?
Rode uma pequena sonda duas vezes como processos separados na plataforma alvo e compare a saída. A sonda abaixo chama o DeriveEncryptionKeys e imprime o salt guardado nos bytes 32 a 47 da entrada /U. Esse valor é escrito à vista em todo arquivo criptografado, então imprimi-lo em logs de CI não divulga nada, mas ele vem do mesmo gerador que a chave de arquivo:
program SaltProbe;
{$mode delphi}
uses
SysUtils, FPdfEncrypt;
var
Opts: TPdfEncryptOptions;
Keys: TPdfEncryptionKeys;
I: Integer;
Hex: string;
begin
Opts := TPdfEncryptOptions.Default;
Opts.UserPassword := 'probe';
Opts.Revision := erR6;
DeriveEncryptionKeys(Opts, Keys);
Hex := '';
for I := 32 to 47 do // /U = hash de 32 bytes + salt de 16 bytes
Hex := Hex + IntToHex(Keys.UEntry[I], 2);
WriteLn(Hex); // precisa diferir a cada execução
end.
Ligue a sonda ao build para todo alvo não Windows: rode duas vezes, e faça o job falhar se as duas linhas baterem. A mesma comparação funciona em arquivos já em circulação. Pegue dois PDFs criptografados escritos por execuções diferentes da mesma aplicação, leia as strings /U dos dicionários Encrypt deles, e compare os últimos 16 bytes; salts idênticos significam que a fonte de chaves é previsível, e esses documentos precisam ser criptografados de novo a partir do plaintext deles para que cada um receba uma chave de arquivo nova. O hábito geral é exercitar caminhos de código não Windows na própria plataforma em vez de confiar na execução Windows, o mesmo raciocínio por trás do backend de timestamp libcurl para builds não Windows
O PDFiumPas é um componente PDF para Delphi e Lazarus construído sobre o motor PDFium, com AES-256, AES-GCM e o token PDF MAC implementados nativamente em Pascal e material de chave vindo do gerador do sistema operacional em toda plataforma. Detalhes e downloads estão na página do componente PDFium para Delphi