Artigo Técnico

Abrir PDFs criptografados com chave bruta no Delphi

O PDF Library for Delphi consegue abrir um PDF criptografado a partir da chave bruta de criptografia do arquivo, em vez de uma senha. DAOpenFileWithEncryptionKey aceita a chave como texto hexadecimal, verifica-a contra o verificador já armazenado no dicionário de criptografia e retorna um handle de Direct Access somente leitura; DAOpenFromStreamWithEncryptionKey faz o mesmo para um TStream pertencente ao chamador. Ambos chegaram na v3.496.0

O cenário é específico e existe de verdade. Um trabalho forense entrega uma chave recuperada de uma imagem de memória, mas nenhuma senha. Uma execução de arquivamento em massa tem dez mil documentos cujas chaves de arquivo ficam em um banco de escrow, porque o sistema DRM de origem deixou de emitir senhas há anos. Uma migração para fora de um produto de gestão de direitos aposentado tem o material de chave, mas nada além disso. Em todos esses casos, a credencial disponível é o resultado da derivação de chave, não a entrada, e nenhum parâmetro de senha da API consegue recebê-la

Por que uma chave de criptografia do arquivo não é uma senha?

Uma senha e uma chave de criptografia do arquivo ficam em lados opostos da derivação de chave no security handler padrão do PDF (ISO 32000-1 §7.6.3). O handler recebe uma senha, combina-a com /O, /P, o file ID e um hash específico da revisão, e produz a chave do arquivo. Coloque uma chave de arquivo no slot da senha e você obterá um nonsense transformado por hash em outro nonsense, por isso esse recurso precisa de um entry point próprio, em vez de uma flag em DAOpenFile

O ponto em que a chave bruta pode ser injetada é determinado pela revisão. As revisões 2 a 4 ainda derivam uma chave distinta por objeto a partir da chave do arquivo, do número do objeto, do número de geração e, no caso de AESV2, do salt AES, portanto possuir a chave do arquivo não permite ignorar a derivação por objeto. As revisões 5 a 7 usam diretamente a chave de arquivo de 32 bytes para AES-256, sem uma etapa por objeto. A única camada comum às duas famílias é a própria chave do arquivo, e é por isso que o PDF Library for Delphi aceita uma chave fornecida externamente somente ali. Se você ainda tem a senha, continue no caminho normal e deixe que o ciclo de retry de senha trate uma primeira tentativa incorreta, porque o entry point de chave bruta abre mão de várias conveniências mantidas pelo caminho da senha

Que entrada DAOpenFileWithEncryptionKey aceita?

Apenas hexadecimal ASCII sem prefixo nem espaços, com número par de caracteres, e a quantidade de bytes decodificados precisa corresponder exatamente à revisão de criptografia. Nas revisões 2 a 4, o tamanho esperado vem de /Length no dicionário de criptografia: um múltiplo de 8 bits entre 5 e 16 bytes, com padrão de 40 bits quando /Length está ausente. Nas revisões 5 a 7, são exatamente 32 bytes, sem negociação. Um prefixo 0x, uma quantidade ímpar de dígitos, mais de 64 caracteres hexadecimais, um bit desconhecido em Options ou um documento que não esteja criptografado produzem o mesmo resultado: handle 0 e LastErrorCode definido como PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, que é 425. A rigidez é intencional. Um parser permissivo que remove espaços e completa a entrada curta com zeros transforma tranquilamente um paste truncado da área de transferência em uma credencial, para só falhar em um ponto muito menos legível

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex tem 32 caracteres hexadecimais para AES-128 R4 e 64 para AES-256 R6
    FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
    If FileHandle= 0 Then
      Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
    Try
      PageRef:= Lib.DAFindPage(FileHandle, 1);
      WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
    Finally
      Lib.DACloseFile(FileHandle);
    End;
  Finally
    Lib.Free;
  End;
End;

Os outros códigos de falha continuam distintos para que um job em lote possa separar erro do operador de problemas na evidência: 411 quando o arquivo não existe, 401 quando não pode ser aberto para leitura e 409 quando a estrutura de referências cruzadas está quebrada. Tudo que é relacionado à chave colapsa em 425, de propósito, pois um entry point de chave bruta que informe qual parte da chave estava errada seria um oracle

O que a verificação realmente prova?

O PDF Library for Delphi prova que a chave fornecida pertence a este documento usando o verificador que o dicionário de criptografia já carrega, e a checagem muda conforme a revisão. A revisão 2 recalcula a criptografia RC4 da string de padding padrão de 32 bytes e compara os 32 bytes com /U. As revisões 3 e 4 fazem hash do padding junto com o file ID, executam a passagem RC4 mais as 19 rodadas derivadas por XOR e comparam os primeiros 16 bytes de /U. As revisões 5 a 7 descriptografam a string /Perms de 16 bytes com IV zero e verificam quatro coisas independentes de uma vez: a palavra de permissões little-endian contra /P, os quatro bytes FF nas posições 5 a 8, a flag de criptografia de metadados como T ou F e o marcador adb nas posições 10 a 12

Quando existe um verificador, mas ele não corresponde, a abertura é recusada sem exceção. Vale dizer isso claramente porque essa é a garantia em que todo o recurso se apoia. Observe também o que a verificação não é: ela diz que a chave descriptografa este arquivo, não que alguém autorizou você a usá-la. A palavra de permissões recuperada de /Perms é evidência sobre a chave, não uma concessão, e se você quiser saber o que o documento de fato declara permitir, essa é uma tarefa separada para uma auditoria de criptografia e permissões. Problemas de normalização do lado da senha, como o tratamento SASLprep de senhas AES-256 não ASCII, simplesmente não surgem aqui, pois nenhuma string chega a um hash

Quando PDF_RAW_KEY_ALLOW_UNVERIFIED se aplica?

PDF_RAW_KEY_ALLOW_UNVERIFIED cobre exatamente uma situação: o documento não tem um verificador utilizável, porque /Perms está ausente ou não tem 16 bytes, ou porque /U é curto demais para comparação. Ele não pode substituir evidência que falha. Corrompa um dígito hexadecimal dentro de /Perms, passe a chave correta com a opção definida e o PDF Library for Delphi ainda retorna 0 e 425. Passe uma chave de 32 bytes zero contra um verificador intacto com a opção definida e a resposta será a mesma. A opção flexibiliza a ausência de prova, nunca uma contradição dela. Como uma abertura de recuperação e uma abertura verificada são estados epistêmicos diferentes, elas também são reportadas separadamente, em vez de serem misturadas no valor de retorno, e DAGetEncryptionKeyValidation recebe o handle aberto e responde com uma de três constantes:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) significa que havia um verificador e ele correspondeu
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) significa que a chave foi aceita somente porque nenhum verificador pôde ser avaliado e o chamador solicitou explicitamente essa política
  • PDF_RAW_KEY_VALIDATION_NONE (0) é o que um handle aberto com senha comum informa
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // nenhum verificador neste arquivo? tente novamente com uma politica de recuperacao explicita
  FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
    PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
  Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
    PDF_RAW_KEY_VALIDATION_VERIFIED:
      Chain.Note('key verified against the encryption dictionary');
    PDF_RAW_KEY_VALIDATION_UNVERIFIED:
      Chain.Note('no verifier available: extraction is unattested');
  End;
End;

Somente leitura por construção, e quem é dono do stream

A entrada de arquivo por chave bruta sempre abre a fonte com fmOpenRead or fmShareDenyWrite e marca toda a cadeia de Direct Access como somente leitura, portanto DAAppendFile recusa a escrita no local e retorna 2, em vez de tentar uma atualização incremental. Isso não é uma política da qual você possa convencer o handle a desistir; ela é definida no construtor antes mesmo de o arquivo ser analisado. Em trabalhos de evidência, a propriedade desejada é que os bytes da fonte sejam idênticos byte a byte depois que o handle for fechado, e a suíte de regressão afirma exatamente isso em um fixture AES-128 de revisão 4 e outro AES-256 de revisão 6. A entrada por stream se comporta da mesma forma nas escritas e acrescenta uma regra: DAOpenFromStreamWithEncryptionKey nunca assume a propriedade, então DACloseFile deixa o seu TStream vivo e você mesmo o libera

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = handle somente leitura: exporte para outro lugar, nunca anexe a evidencia
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // o handle nunca foi dono deste stream
End;

Higiene da chave e o que a DLL exporta

Fechar a cadeia sobrescreve a chave do arquivo, o cache de senhas e as chaves derivadas dos objetos, e a chave decodificada é apagada no bloco Finally do próprio entry point, tenha a abertura sido bem-sucedida ou não. Há uma sutileza por trás disso que consumiu tempo real de depuração: qualquer cópia que precise sobreviver é clonada explicitamente com SetLength mais Move, em vez de ser atribuída. Atribua uma AnsiString a outra no Delphi e os dois nomes compartilham um buffer por copy-on-write, então apagar o lado do chamador zeraria a chave que o handler de criptografia ainda está usando, e o documento seria descriptografado como lixo por razões que nenhum stack trace explicaria. Somente os entry points baseados em arquivo atravessam a fronteira da DLL, nas formas wide e ANSI, junto com o accessor do status de validação; a variante TStream permanece exclusiva do Delphi, pois depende do tempo de vida e da semântica de referências de objetos Delphi, sem representação honesta em uma ABI C plana. Se sua ferramenta de recuperação for cliente de uma DLL, planeje preparar um arquivo temporário e apagá-lo sob os mesmos controles aplicados à chave

Trate a chave hexadecimal como material de credencial, com as regras de manuseio que você daria a uma senha de documento, e mantenha o status de validação no log produzido pela sua cadeia de custódia, para que um leitor posterior possa distinguir uma extração verificada de uma não atestada. Se você está avaliando um componente PDF Delphi para perícia, arquivamento em massa ou migração de DRM, os entry points de chave bruta, a garantia de somente leitura e a superfície de auditoria da criptografia fazem parte da mesma biblioteca, e a lista completa de recursos está na página do produto PDF Library for Delphi