Artigo Técnico

Encriptação AES-256 de PDF a Alta Velocidade para Documentos Massivos

Criptografar um PDF de 2 GB parece um problema de streaming: abrir o ficheiro, passar dois gigabytes pelo AES-256, escrever o resultado. Esse modelo mental está errado de uma forma que decide todo o orçamento de desempenho. A ISO 32000-1 §7.6 define a granularidade da criptografia de PDF no objeto individual — cada fluxo e cada string são criptografados separadamente, cada um com seu próprio vetor de arranque e seu próprio preenchimento. Um ficheiro compactado de 2 GB com 500.000 objetos são 500.000 pequenas operações CBC, não uma longa passagem, e nessa escala o custo fixo em torno de cada operação importa mais do que a aritmética AES dentro dela

Este artigo é sobre esse custo fixo: para onde vai o tempo quando o código Delphi aplica o AES-256 em documentos muito grandes e como recuperá-lo. Para o lado da configuração — palavras-passe, sinalizadores de permissão, a chamada de compatibilidade da revisão 5 versus 6 — consulte o artigo complementar sobre configurar criptografia AES-256 no HotPDF; nada disso se repete aqui

Meio milhão de operações CBC, não uma passagem

O esqueleto do ficheiro permanece em texto simples. Tabelas de referência cruzada, números de objetos, chaves de dicionário, a árvore de páginas: nada disso é criptografado, e é assim que um leitor pode localizar objetos antes de validar uma palavra-passe. O que o padrão criptografa é o conteúdo — dados de fluxo, como descrições de página, imagens, fontes e anexos, além de strings como valores de metadados e texto de anotação. Sob o filtro de criptografia AES-256, cada um é processado por conta própria: um novo IV aleatório de 16 bytes, CBC sobre os bytes, preenchimento de bloco para um limite de 16 bytes, e o IV gravado de forma clara à frente do texto cifrado

Duas consequências se seguem. Primeiro, o texto cifrado é sempre mais longo que o texto simples: o IV adiciona 16 bytes e o preenchimento adiciona mais de 1 a 16, portanto, uma string de 100 bytes ocupa 128 bytes no disco e um fluxo vazio ainda produz 32. O código que dimensiona o buffer de saída para o comprimento da entrada ou reescreve apenas tantos bytes quanto leu produz ficheiros que falham na descriptografia no último bloco de cada objeto. Segundo, o custo acompanha a contagem de objetos, não apenas a contagem de bytes. Um ficheiro compactado concentra seus bytes em alguns grandes fluxos de imagem, mas carrega centenas de milhares de fluxos curtos e strings pequenas onde a sobrecarga por operação, não o AES, é a conta principal

A única misericórdia no design do AES-256 é o manuseio de chaves. Manipuladores de segurança até a revisão 4 derivavam uma chave distinta para cada objeto combinando a chave do ficheiro com os números do objeto e da geração, forçando um novo cronograma de chave a cada vez. Os esquemas /V 5 abandonaram a derivação por objeto: uma chave de ficheiro aleatória de 256 bits criptografa cada objeto no documento. Esse fato autoriza todas as otimizações abaixo — o caro estado criptográfico pode ser construído uma vez por ficheiro, não uma vez por objeto

O dicionário R6 /Encrypt: uma abertura lenta, objetos baratos

Um documento da revisão 6 declara seu esquema no dicionário /Encrypt do trailer, e as entradas que importam cabem em algumas linhas:

__CODE_BLOCK_

/V 5 seleciona a arquitetura de chave de 256 bits e /R 6 o handshake fortalecido da ISO 32000-2. /CF define o filtro de criptografia nomeado — /AESV3 significa AES-256 no modo CBC com o IV prefixado — e /StmF e /StrF atribuem esse filtro a fluxos e strings, respectivamente. /O, /U, /OE e /UE contêm o material de verificação de palavra-passe e empacotamento de chave, e /Perms carrega uma cópia criptografada por AES dos bits de permissão para que um editor hostil não possa inverter /P silenciosamente

A estrutura de custo se esconde em /OE e /UE. Desempacotar a chave do ficheiro deles executa o Algoritmo 2.B, uma função de derivação de chave iterada encadeando rodadas SHA-256, SHA-384 e SHA-512 — pelo menos 64 delas, com uma regra de parada dependente de dados — construída deliberadamente lenta para que a adivinhação de palavras-passe continue cara. Esse preço é pago uma vez quando o gravador produz o ficheiro e uma vez quando um leitor o abre, milissegundos de um dígito cada. Em um ficheiro de meio milhão de objetos, o KDF é ruído, e se um salvamento for lento, o Algoritmo 2.B não é o suspeito; o loop por objeto é

Reutilize o handle da chave, reutilize o buffer de rascunho

A implementação ingênua é uma função utilitária organizada: um ajudante EncryptAes256Cbc que abre o provedor CNG do Windows, seleciona CBC, gera o objeto de chave, criptografa um buffer e destrói tudo. Correto, testável por unidade e desastroso dentro de um loop de 500.000 iterações. A documentação da Microsoft sinaliza BCryptOpenAlgorithmProvider como caro e recomenda fazer o cache do handle, e BCryptGenerateSymmetricKey executa o cronograma de chave AES completo e aloca o estado do provedor — puro desperdício quando a chave nunca muda em todo o documento

A RTL do Delphi não vem com nenhuma unidade de importação bcrypt, portanto, declare os pontos de entrada diretamente. A classe abaixo constrói todo o estado criptográfico uma vez e depois criptografa qualquer número de objetos sem alocação em estado estacionário:

__CODE_BLOCK_

Três detalhes são fundamentais. A consulta de tamanho — a primeira chamada de BCryptEncrypt, com um buffer de saída nulo — retorna o comprimento do texto cifrado preenchido, nunca igual ao comprimento de entrada; o preenchimento é determinístico, então você pode computar ((Len div 16) + 1) * 16 sozinho e reduzir pela metade a contagem de chamadas, mas a consulta é o contrato documentado. Segundo, BCryptEncrypt avança o buffer IV no local enquanto é encadeado, de forma que uma cópia de trabalho vai para cada chamada e o IV original vai para a saída. Terceiro, FScratch apenas cresce, até o maior objeto do ficheiro, após o qual o loop não aloca nada

O que vale a pena reutilizar um handle, medido

O ficheiro que forçou este exercício foi um ficheiro de empréstimo digitalizado de 1,8 GB: 412.000 objetos criptografados carregando 1.710 MB de carga útil depois que a estrutura de texto simples é subtraída. Mesma máquina, mesmo ficheiro, armazenamento NVMe, uma thread:

  • Configuração por chamada (provedor aberto e chave gerada dentro do auxiliar): fase de criptografia 71,3 s — 1.710 MB ÷ 71,3 s ≈ 24 MB/s
  • Estado elevado (a classe acima): 9,6 s — 1.710 MB ÷ 9,6 s ≈ 178 MB/s

A diferença é de 61,7 s em 412.000 chamadas, ou cerca de 150 µs por chamada gasto abrindo um provedor, definindo um modo de encadeamento e reconstruindo um cronograma de chave para uma chave que nunca mudou. Nada disso era criptografia. Com o AES-NI, a criptografia CBC de grandes buffers roda perto de 1,4 GB/s em um núcleo, então a aritmética AES em si é responsável por cerca de 1,2 s dos 9,6; a maior parte do restante são as duas transições de modo de utilizador BCryptEncrypt por objeto mais a geração de IV por objeto. O processamento em lote dos IVs — uma chamada de BCryptGenRandom preenchendo 4.096 deles — reduziu a execução para 8,9 s. Passado isso, você está no limite por objeto da API, e a alavanca restante é o paralelismo: os objetos /V 5 são independentes sob a chave de ficheiro compartilhada, portanto, quatro threads de trabalho com um objeto de chave cada levaram a fase para 3,1 s antes que o gravador de saída se tornasse o ponto de serialização

Reescrita completa versus salvamento incremental

A granularidade também decide quanto custa um salvamento. Adicionar criptografia a um documento de texto simples existente reescreve cada objeto por definição: cada fluxo e string altera o conteúdo e o comprimento, cada deslocamento de referência cruzada se move e não existe um caminho incremental. Planeje isso como uma reescrita sequencial completa e grave em um ficheiro temporário que seja renomeado sobre o alvo, porque, de outra forma, uma falha na metade da criptografia deixa um ficheiro semi-cifrado que nenhuma palavra-passe abrirá

A direção oposta é a barata. Depois que um ficheiro é criptografado, uma atualização incremental anexa novos objetos criptografados com a mesma chave de ficheiro e deixa cada byte original intocado. Carimbar uma anotação de aprovação em um ficheiro criptografado de 2 GB custa kilobytes de saída anexada, não uma reescrita de 2 GB. O corolário do pipeline: criptografe uma vez, como o último passo do trabalho, e deixe que os toques subsequentes aproveitem salvamentos incrementais. Uma rotação de palavra-passe que também rotaciona a chave do ficheiro é uma reescrita completa novamente — programe-a como tal

Medindo o rendimento sem se enganar

As alegações de rendimento de criptografia tendem a estar erradas no numerador, no denominador ou em ambos. O numerador deve ser os bytes de carga útil: a soma dos comprimentos de fluxo e string efetivamente passados pelo AES, após a compactação, que o gravador pode totalizar à medida que avança. O tamanho do ficheiro superestima — o ficheiro compactado acima tem 1,8 GB em disco, mas apenas 1.710 MB dele tocam a cifra. O denominador deve ser apenas a fase de criptografia, delimitada com TStopwatch de System.Diagnostics, com a análise sintática, deflate e I/O de disco fora dos delimitadores. Se você incluir isso, o mesmo código de criptografia será medido várias vezes mais lento em um ficheiro que simplesmente compacta de maneira pior. Os números acima são comparáveis ​​precisamente porque ambos os lados da divisão são apenas criptografia

Nada disso precisa ser código de sua propriedade. O HotPDF envolve a mesma engenharia em propriedades do componente — ActivateProtection, CryptKeyLength, UseAES256R6 — na altitude certa para aplicações VCL interativas, com as armadilhas da ordem de atribuição abordadas no artigo AES-256 do HotPDF. Para pipelines não supervisionados, o PDFlibPas aplica a revisão 6 do AES-256 a ficheiros existentes em uma única chamada EncryptFile na Força 4 e verifica depois o que parou no disco, um fluxo de trabalho detalhado no artigo de auditoria de criptografia PDFlibPas

Os caminhos de criptografia descritos aqui acompanham o HotPDF Component para Delphi e C++Builder e na biblioteca PDFlibPas; ambas as páginas de produtos trazem a referência completa de criptografia