Pegue um PDF de fatura que já carrega criptografia AES-256 e peça ao Componente PDFium para Delphi e C++Builder (PDFiumPas) para carimbá-lo como PDF/A para retenção de arquivamento, ou assiná-lo com PAdES, por meio de uma atualização incremental, em vez de uma reescrita completa. A biblioteca não vai chegar lá corrigindo os bytes criptografados diretamente: seus seis injetores de marcador de conformidade detectam uma entrada /Encrypt existente e passam a origem para o destino byte a byte inalterada, e seu assinador PAdES levanta uma exceção em vez de emitir uma assinatura que nenhum validador vai aceitar
Essa é uma questão diferente de auditar um PDF que você não criou em busca de risco oculto, que é seu próprio exercício somente leitura. Este artigo é sobre o lado de escrita da mesma fronteira de confiança: o que seu próprio código tem permissão de fazer a um arquivo cujos bytes já estão trancados atrás da senha de outra pessoa, no instante em que esse código tenta adicionar qualquer coisa a ele posteriormente
O que a ISO 32000-1 exige quando você atualiza um PDF criptografado?
A ISO 32000-1 §7.5.6 exige que o trailer de uma atualização incremental repita cada entrada do trailer anterior, exceto /Prev, e a Tabela 15 lista /Encrypt entre as entradas que um trailer pode carregar. Descarte-a do novo trailer e um leitor em conformidade não tem motivo para duvidar da omissão: o trailer mais novo é autoritativo, então um leitor que não encontra nenhum /Encrypt ali decide que o arquivo inteiro não está criptografado e tenta analisar o corpo mais antigo, ainda cifrado, como bytes simples. Mantenha /Encrypt no novo trailer, mas escreva os próprios objetos da atualização como texto simples, e a falha só se move um passo adiante: o leitor detecta corretamente a criptografia, executa cada objeto que toca através da cifra do arquivo, incluindo os novos que nunca foram criptografados para começo de conversa, e recebe ruído de volta para conteúdo que era perfeitamente legível antes de a descriptografia tocá-lo. Qualquer um dos dois erros produz um arquivo que parece uma atualização incremental normal e bem formada em nível de byte, até que um leitor em conformidade o abra
Seis injetores de marcador, um portão de criptografia da v2.14.2
O PDFiumPas vem com seis injetores de marcador em nível de byte, um para cada subconjunto ISO de PDF que consegue rotular: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1), e PDF/VT-1 (ISO 16612-2). Cada um pega os bytes que o próprio FPDF_SaveAsCopy do PDFium já escreveu e coloca em camada uma segunda atualização incremental, menor, sobre eles: um novo stream de metadados XMP, uma edição de dicionário de catálogo que aponta para ele, e para os subconjuntos orientados a impressão, um OutputIntent e um perfil ICC. A partir da v2.14.2, cada um de InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, e InjectPdfVTMarkers lê o trailer de origem primeiro, e se ele reporta uma entrada /Encrypt existente, copia a origem para o destino sem modificação e retorna imediatamente. Nenhum XMP, nenhum OutputIntent, nenhuma edição de catálogo — quem chama recebe o arquivo original de volta, byte a byte
var
Src, Dst: TFileStream;
Opts: TPdfXSaveOptions;
begin
Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
try
Opts.Conformance := pxc4;
InjectPdfXMarkers(Src, Dst, Opts);
// pdfx-attempt.pdf is byte-identical to the source: still encrypted,
// no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
// nothing was corrupted either
finally
Dst.Free;
Src.Free;
end;
end;
Ter permissão de estar criptografado não é a mesma coisa que ser seguro para injetar
PDF/E-1 e PDF/R-1 ambos permitem explicitamente que seu documento hospedeiro seja criptografado no nível da especificação, o que parece uma isenção até você olhar o que de fato precisa acontecer no disco. A ISO 24517-1 §6.3 permite criptografia para PDF/E-1, e a ISO 23504-1 §6.2.3 a permite para PDF/R-1, desde que o cabeçalho declare %PDF-2.0. Nenhuma das cláusulas diz nada sobre se um pós-processador em nível de byte consegue adicionar com segurança um objeto de texto simples a esse contêiner criptografado, e não consegue, pelos mesmos motivos da §7.5.6 que se aplicam a todo outro subconjunto. Os próprios validadores de conformidade do PDFiumPas para esses dois perfis, ValidatePdfECompliance e ValidatePdfRCompliance, registram a presença de /Encrypt deliberadamente sem sinalizá-la como um defeito, o que é correto para um validador somente leitura que nunca escreve um byte. Também é um padrão fácil de passar por cima e assumir que o injetor irmão não precisa de uma proteção separada, quando o injetor é a única função no par que de fato precisa se recusar
O SaveAsPdfX descriptografa silenciosamente seu documento?
Sim, sempre que você passa pelos métodos de conveniência públicos, em vez de chamar um injetor diretamente. Cada um de TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, e SaveAsPdfVT renderiza o documento atual para um stream temporário com SaveAs(Tmp, saRemoveSecurity) antes de entregar esses bytes ao seu injetor correspondente. saRemoveSecurity mapeia para a própria flag FPDF_REMOVE_SECURITY do PDFium, de modo que a cópia temporária que o injetor recebe nunca foi criptografada para começo de conversa, e a proteção /Encrypt do injetor nunca tem motivo para disparar. A saída carrega seus marcadores PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, ou PDF/VT-1, mas não está mais protegida por qualquer senha que abriu a origem
Essa compensação é invisível até alguém rio abaixo abrir a cópia arquivística "protegida" sem senha e perceber que simplesmente funciona. A correção não é uma chamada de método diferente; o PDFiumPas não tem um saAddSecurity equivalente para parear com saRemoveSecurity, porque o motor PDFium subjacente nunca foi construído para escrever nova criptografia, apenas para removê-la. Se ambas as propriedades importam para um arquivo, a criptografia precisa ser uma etapa separada que você mesmo possui, aplicada depois dos marcadores de conformidade, e não dobrada na mesma chamada SaveAsPdfA
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret'; // needed to open the source at all
Pdf.FileName := 'signed-encrypted-invoice.pdf';
Pdf.LoadDocument;
Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
// invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
// ran first inside SaveAsPdfA: the output opens without a password
finally
Pdf.Free;
end;
end;
O que acontece quando você assina um PDF criptografado com PAdES?
O PDFiumPas se recusa completamente, em vez de silenciosamente descartar a requisição da forma como um injetor de marcador faz. TPdf.SignPades e SignPadesToStream ambos roteiam por meio de um SignPadesBytes interno, e a primeira coisa que ele faz depois de analisar o trailer de origem é verificar por /Encrypt. Se a entrada está presente, ele levanta EPadesCrypto com a mensagem "SignPadesBytes: the source document is encrypted; remove encryption before signing" em vez de prosseguir mais adiante. InjectPadesDssMarkers, a função que embute certificados, respostas OCSP e CRLs para validação de longo prazo, aplica a verificação idêntica pelo motivo idêntico, com sua própria mensagem: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
O raciocínio aqui é mais rígido que o passe-direto dos injetores de marcador, e deliberadamente assim. Um passe-direto silencioso é seguro para um carimbo PDF/A porque pulá-lo deixa você com o mesmo PDF válido com que começou, apenas sem rótulo. Assinar não pode falhar tão silenciosamente: uma assinatura que silenciosamente nunca foi adicionada parece, para qualquer código chamador que só verifica um resultado booleano, exatamente como uma assinatura que foi adicionada com sucesso. EPadesCrypto descende da classe Exception comum, de modo que capturá-la é tratamento de exceção normal, não uma convenção especial de fluxo de controle que você precisa aprender
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret';
Pdf.FileName := 'encrypted-contract.pdf';
Pdf.LoadDocument;
try
Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
except
on E: EPadesCrypto do
// E.Message: 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
end;
finally
Pdf.Free;
end;
end;
Sequenciando carimbos de conformidade, assinaturas e criptografia
A correção prática é ordenação, não uma biblioteca diferente. Aplique marcadores PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, ou PDF/VT-1 primeiro, adicione qualquer assinatura PAdES em seguida, e só então rode qualquer etapa em seu pipeline que de fato possua a criptografia, seja isso um escritor de PDF dedicado, um aparelho de assinatura, ou sua própria implementação AES. A camada de atualização incremental do PDFiumPas se encaixa naturalmente no meio dessa sequência, anexando objetos pequenos e específicos a um arquivo que já está por outro lado finalizado, e a criptografia pertence ao final precisamente porque é a única operação na cadeia que o próprio PDFiumPas não consegue realizar nem reverter
Nada disso muda como o PDFiumPas lê os dados de trailer e referência cruzada dos quais toda atualização incremental depende, que é sua própria fonte de sutileza uma vez que streams xref entram em cena; validando os streams de objeto e xref de um PDF cobre como esse mesmo caminho de leitura de trailer trata estruturas comprimidas do PDF 1.5+. E uma vez que um documento está pronto para algo mais forte que um carimbo de conformidade, assinando um PDF com uma assinatura PAdES B-B em Delphi é onde SignPades assume exatamente do ponto onde este artigo para
Os injetores de marcador e métodos SignPades descritos aqui vêm como parte do Componente PDFium para Delphi e C++Builder, ao lado da renderização e inspeção somente leitura que o PDFium fornece nativamente