Artigo Técnico

Não Se Pode Corrigir Silenciosamente um PDF Encriptado em Delphi

Pegue-se numa fatura em PDF que já transporta encriptação AES-256 e peça-se ao PDFium Component para Delphi e C++Builder (PDFiumPas) para a rotular como PDF/A para retenção em arquivo, ou para a assinar com PAdES, através de uma atualização incremental em vez de uma reescrita completa. A biblioteca não o consegue fazer corrigindo diretamente os bytes encriptados: os seus seis injetores de marcadores de conformidade detetam uma entrada /Encrypt já existente e passam a origem para o destino byte a byte, sem qualquer alteração, e o seu assinador PAdES levanta uma exceção em vez de emitir uma assinatura que nenhum validador aceitará

Essa é uma questão diferente de auditar um PDF que não se criou à procura de riscos ocultos, o que é um exercício próprio, apenas de leitura. Este artigo trata do lado da escrita da mesma fronteira de confiança: o que o próprio código de quem programa tem permissão para fazer a um ficheiro cujos bytes já estão trancados atrás da palavra-passe de outra pessoa, no momento em que esse código tenta acrescentar-lhe qualquer coisa a posteriori

O que exige a ISO 32000-1 ao atualizar um PDF encriptado?

A ISO 32000-1 §7.5.6 exige que o trailer de uma atualização incremental repita todas as entradas do trailer anterior, exceto /Prev, e a Tabela 15 lista /Encrypt entre as entradas que um trailer pode transportar. Se essa entrada for omitida do novo trailer, um leitor conforme não tem qualquer motivo para duvidar da omissão: o trailer mais recente é o autoritativo, pelo que um leitor que aí não encontre /Encrypt decide que o ficheiro inteiro não está encriptado e tenta interpretar o corpo mais antigo, ainda cifrado, como bytes simples. Se se mantiver /Encrypt no novo trailer mas se escreverem os objetos da atualização como texto simples, a falha limita-se a mudar de sítio, um passo mais tarde: o leitor deteta corretamente a encriptação, passa cada objeto que toca pela cifra do ficheiro, incluindo os novos que nunca chegaram a ser encriptados, e recebe ruído de volta para conteúdo que era perfeitamente legível antes de a desencriptação lhe tocar. Qualquer um dos dois erros produz um ficheiro que, ao nível dos bytes, parece uma atualização incremental normal e bem formada, até que um leitor conforme o abra

Seis injetores de marcadores, um único portão de encriptação na v2.14.2

O PDFiumPas distribui seis injetores de marcadores ao nível dos bytes, 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 recebe os bytes que o próprio FPDF_SaveAsCopy do PDFium já escreveu e sobrepõe-lhes uma segunda atualização incremental, mais pequena: uma nova stream de metadados XMP, uma edição ao dicionário do catálogo que a aponta, e, para os subconjuntos orientados à 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ê primeiro o trailer de origem, e, se reportar uma entrada /Encrypt já existente, copia a origem para a stream de destino sem qualquer modificação e regressa de imediato. Sem XMP, sem OutputIntent, sem edição ao catálogo — o chamador recebe de volta o ficheiro original, 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;

Ser permitido estar encriptado não é o mesmo que ser seguro injetar-lhe conteúdo

O PDF/E-1 e o PDF/R-1 permitem ambos, explicitamente, que o seu documento hospedeiro esteja encriptado ao nível da especificação, o que soa a isenção até se olhar para o que efetivamente tem de acontecer no disco. A ISO 24517-1 §6.3 permite encriptação para o PDF/E-1, e a ISO 23504-1 §6.2.3 permite-a para o PDF/R-1 desde que o cabeçalho declare %PDF-2.0. Nenhuma destas cláusulas diz seja o que for sobre se um pós-processador ao nível dos bytes pode acrescentar em segurança um objeto em texto simples a esse contentor encriptado, e não pode, pelas mesmas razões do §7.5.6 que se aplicam a todos os outros subconjuntos. Os próprios validadores de conformidade do PDFiumPas para estes dois perfis, ValidatePdfECompliance e ValidatePdfRCompliance, registam a presença de /Encrypt deliberadamente sem a assinalar como defeito, o que está correto para um validador só de leitura que nunca escreve um único byte. É também um padrão fácil de ler na diagonal e presumir que o injetor irmão não precisa de uma proteção separada, quando o injetor é precisamente a função do par que efetivamente tem de recusar

O SaveAsPdfX desencripta silenciosamente o documento?

Sim, sempre que se passe pelos métodos de conveniência públicos em vez de se chamar um injetor diretamente. Cada um de TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, e SaveAsPdfVT renderiza o documento atual para uma stream temporária com SaveAs(Tmp, saRemoveSecurity) antes de entregar esses bytes ao injetor correspondente. O saRemoveSecurity corresponde à própria flag FPDF_REMOVE_SECURITY do PDFium, pelo que a cópia temporária que o injetor recebe nunca esteve encriptada em primeiro lugar, e a proteção /Encrypt do injetor nunca tem motivo para disparar. A saída transporta os marcadores PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, ou PDF/VT-1, mas deixou de estar protegida pela palavra-passe que abria a origem

Essa contrapartida é invisível até alguém, a jusante, abrir a cópia de arquivo "protegida" sem palavra-passe e reparar que simplesmente funciona. A correção não é uma chamada de método diferente; o PDFiumPas não tem nenhum equivalente saAddSecurity para emparelhar com saRemoveSecurity, porque o motor PDFium subjacente nunca foi construído para escrever nova encriptação, apenas para a remover. Se ambas as propriedades importarem para um ficheiro, a encriptação tem de ser um passo separado, da responsabilidade de quem programa, aplicado depois dos marcadores de conformidade, e não incorporado na mesma chamada a 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 ao assinar um PDF encriptado com PAdES?

O PDFiumPas recusa-se por completo, em vez de descartar silenciosamente o pedido da forma como um injetor de marcadores faz. Tanto TPdf.SignPades como SignPadesToStream passam por uma SignPadesBytes interna, e a primeira coisa que esta faz depois de interpretar o trailer de origem é verificar a presença de /Encrypt. Se a entrada estiver presente, levanta EPadesCrypto com a mensagem "SignPadesBytes: the source document is encrypted; remove encryption before signing" em vez de continuar. O InjectPadesDssMarkers, a função que incorpora certificados, respostas OCSP, e CRLs para validação a longo prazo, aplica a verificação idêntica pela mesma razão, com a sua própria mensagem: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

O raciocínio aqui é mais rigoroso do que a simples passagem direta dos injetores de marcadores, e deliberadamente assim. Uma passagem direta silenciosa é segura para um carimbo PDF/A porque saltá-la deixa exatamente o mesmo PDF válido com que se começou, apenas sem rótulo. Assinar não pode falhar de forma tão silenciosa: uma assinatura que silenciosamente nunca foi acrescentada parece, para qualquer código chamador que só verifique um resultado booleano, exatamente igual a uma assinatura que foi acrescentada com sucesso. A EPadesCrypto descende da classe Exception comum, pelo que apanhá-la é tratamento de exceções normal, não uma convenção de fluxo de controlo especial que seja preciso 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;

Sequenciar carimbos de conformidade, assinaturas, e encriptação

A correção prática é uma questão de ordem, não uma biblioteca diferente. Aplicar primeiro os marcadores PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, ou PDF/VT-1, acrescentar de seguida qualquer assinatura PAdES, e só depois correr o que quer que seja, no pipeline de quem programa, que efetivamente trate da encriptação, seja isso um escritor de PDF dedicado, um dispositivo de assinatura, ou a própria implementação de AES. A camada de atualização incremental do PDFiumPas encaixa naturalmente a meio dessa sequência, acrescentando objetos pequenos e específicos a um ficheiro que, de resto, já está terminado, e a encriptação pertence ao fim precisamente porque é a única operação da cadeia que o próprio PDFiumPas não consegue realizar nem reverter

Nada disto altera a forma como o PDFiumPas lê os dados de trailer e de referência cruzada de que toda a atualização incremental depende, o que é, por si só, uma fonte de subtileza assim que as streams xref entram em cena; validar as streams de objetos e xref de um PDF aborda como esse mesmo caminho de leitura do trailer trata as estruturas comprimidas do PDF 1.5+. E assim que um documento estiver pronto para algo mais forte do que um carimbo de conformidade, assinar um PDF com uma assinatura PAdES B-B em Delphi é o ponto onde o SignPades assume exatamente a partir de onde este artigo termina

Os injetores de marcadores e os métodos SignPades aqui descritos fazem parte do PDFium Component para Delphi e C++Builder, a par da renderização e inspeção só de leitura que o PDFium fornece nativamente