O PDF Library for Delphi consegue abrir um PDF encriptado a partir da chave de encriptação bruta do ficheiro em vez de uma palavra-passe. DAOpenFileWithEncryptionKey aceita a chave como texto hexadecimal, verifica-a contra o verificador já guardado no dicionário de encriptação e devolve um handle de Direct Access só de leitura; DAOpenFromStreamWithEncryptionKey faz o mesmo para um TStream pertencente ao chamador. Ambos foram introduzidos na v3.496.0
O cenário é específico e é real. Uma perícia entrega-lhe uma chave recuperada de uma imagem de memória e nenhuma palavra-passe. Um processo de arquivo em massa tem dez mil documentos cujas chaves de ficheiro estão numa base de dados de escrow porque o sistema de DRM de origem deixou de emitir palavras-passe há anos. Uma migração de um produto de gestão de direitos descontinuado tem material de chaves e nada mais. Em todos esses casos, a credencial que tem é o resultado da derivação de chaves, não a entrada, e nenhum parâmetro de palavra-passe em qualquer API a aceitará
Porque é que uma chave de encriptação de ficheiro não é uma palavra-passe?
Uma palavra-passe e uma chave de encriptação de ficheiro estão em lados opostos da derivação de chaves no security handler padrão do PDF (ISO 32000-1 §7.6.3). O handler recebe uma palavra-passe, combina-a com /O, /P, o ID do ficheiro e um hash específico da revisão, e produz a chave do ficheiro. Introduza uma chave de ficheiro no lugar da palavra-passe e obterá nonsense transformado em outro nonsense, razão pela qual isto precisa de um ponto de entrada próprio em vez de um sinalizador em DAOpenFile
O local onde a chave bruta pode ser injetada é definido pela revisão. As revisões 2 a 4 continuam a derivar uma chave distinta por objeto a partir da chave do ficheiro, do número do objeto, do número de geração e, no caso de AESV2, do salt AES, pelo que ter a chave do ficheiro não permite de modo algum saltar a derivação ao nível do objeto. As revisões 5 a 7 usam diretamente a chave de ficheiro de 32 bytes para AES-256, sem uma etapa por objeto. A única camada comum às duas é a própria chave do ficheiro, e é por isso que o PDF Library for Delphi só aceita uma chave fornecida externamente nesse ponto. Se ainda tiver a palavra-passe, mantenha o percurso normal e deixe o ciclo de tentativas de palavra-passe tratar a primeira tentativa errada, porque o ponto de entrada de chave bruta abdica deliberadamente de várias conveniências que o percurso da palavra-passe mantém
Que entrada aceita DAOpenFileWithEncryptionKey?
Apenas hexadecimal ASCII sem prefixo nem espaços, com comprimento par, e o número de bytes descodificados tem de corresponder exatamente à revisão de encriptação. Nas revisões 2 a 4, o comprimento esperado vem de /Length no dicionário de encriptação: um múltiplo de 8 bits entre 5 e 16 bytes, com o valor predefinido de 40 bits quando /Length está ausente. Nas revisões 5 a 7 são exatamente 32 bytes, sem negociação. Um prefixo 0x, um número ímpar de dígitos, mais de 64 caracteres hexadecimais, um bit desconhecido em Options ou um documento que não esteja encriptado produzem todos o mesmo resultado: um handle 0 e LastErrorCode definido como PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, que é 425. A rigidez é intencional. Um parser permissivo que remova espaços e preencha com zeros uma entrada curta transformará alegremente uma colagem truncada da área de transferência numa credencial e falhará depois num 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 trabalho em lote possa separar erro do operador de problemas na evidência: 411 quando o ficheiro não existe, 401 quando não pode ser aberto para leitura, 409 quando a estrutura de referências cruzadas está danificada. Tudo o que está relacionado com a chave colapsa em 425, deliberadamente, porque um ponto de entrada de chave bruta que comunique qual parte da chave estava errada seria um oráculo
O que prova realmente a verificação?
O PDF Library for Delphi prova que a chave fornecida pertence a este documento, usando o verificador que o dicionário de encriptação já contém, e a verificação varia conforme a revisão. A revisão 2 recalcula a encriptação RC4 da cadeia de padding padrão de 32 bytes e compara os 32 bytes com /U. As revisões 3 e 4 aplicam hash ao padding juntamente com o ID do ficheiro, executam a passagem RC4 mais as 19 rondas derivadas por XOR e comparam os primeiros 16 bytes de /U. As revisões 5 a 7 desencriptam a cadeia /Perms de 16 bytes com um IV a zero e verificam quatro coisas independentes de uma só vez: a palavra de permissões little-endian contra /P, os quatro bytes FF nas posições 5 a 8, o sinalizador de encriptação 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 incondicionalmente. Vale a pena dizê-lo sem rodeios, porque é a garantia em que assenta toda a funcionalidade. Repare também no que a verificação não é: diz que a chave desencripta este ficheiro, não que alguém o autorizou a utilizá-la. A palavra de permissões recuperada de /Perms é evidência sobre a chave, não uma concessão, e se quiser saber o que o documento afirma realmente permitir, isso é uma tarefa separada para uma auditoria de encriptação e permissões. Problemas de normalização do lado da palavra-passe, como o tratamento SASLprep de palavras-passe AES-256 não ASCII, simplesmente não surgem aqui, porque nenhuma cadeia chega a um hash
Quando se aplica PDF_RAW_KEY_ALLOW_UNVERIFIED?
PDF_RAW_KEY_ALLOW_UNVERIFIED cobre exatamente uma situação: o documento não contém um verificador utilizável, porque /Perms está ausente ou não tem 16 bytes, ou porque /U é demasiado curto para comparar. Não pode sobrepor-se a evidência falhada. Corrompa um dígito hexadecimal dentro de /Perms e forneça a chave correta com a opção definida: o PDF Library for Delphi continua a devolver 0 e 425. Forneça uma chave de 32 bytes a 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 da prova. Como uma abertura de recuperação e uma abertura verificada são estados epistémicos diferentes, também são comunicadas separadamente em vez de serem fundidas 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 existia um verificador e que correspondiaPDF_RAW_KEY_VALIDATION_UNVERIFIED(2) significa que a chave foi aceite apenas porque não era possível avaliar nenhum verificador e o chamador pediu explicitamente essa políticaPDF_RAW_KEY_VALIDATION_NONE(0) é o que um handle aberto com uma palavra-passe normal comunica
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
(Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
// não há verificador neste ficheiro? tente novamente com uma política de recuperação explícita
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;
Só de leitura por construção, e quem é dono do stream
O ponto de entrada de ficheiro com chave bruta abre sempre a origem com fmOpenRead or fmShareDenyWrite e marca toda a cadeia de Direct Access como só de leitura, pelo que DAAppendFile recusa a escrita no local e devolve 2 em vez de tentar uma atualização incremental. Não é uma política de que possa convencer o handle a abdicar; fica definida no construtor antes de o ficheiro sequer ser analisado. Para trabalho de evidência, a propriedade que interessa é que os bytes de origem permaneçam idênticos byte a byte depois de o handle fechar, e a suite de regressão verifica exatamente isso tanto num fixture AES-128 da revisão 4 como num AES-256 da revisão 6. A entrada por stream comporta-se da mesma forma nas escritas e acrescenta mais uma regra: DAOpenFromStreamWithEncryptionKey nunca assume a posse, por isso DACloseFile deixa o seu TStream vivo e é você que o liberta
Source:= TMemoryStream.Create;
Try
Source.LoadFromFile(ArchivePath);
Source.Position:= 0;
FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
If FileHandle<> 0 Then
Try
// 2 = handle só de leitura: exporte noutro local, nunca acrescente à evidência
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 substitui a chave do ficheiro, a cache da palavra-passe e as chaves de objeto derivadas, e a chave descodificada é apagada no bloco Finally do próprio ponto de entrada, quer a abertura tenha tido sucesso quer não. Há uma subtileza por trás disso que consumiu tempo real de debugging: qualquer cópia que tenha de sobreviver é clonada explicitamente com SetLength e Move, em vez de ser atribuída. Atribua uma AnsiString a outra em Delphi e os dois nomes partilharão um buffer em copy-on-write, pelo que apagar o lado do chamador colocaria a zero a chave que o crypt handler ainda está a usar e o documento seria desencriptado para lixo sem que qualquer stack trace explicasse porquê. Apenas os pontos de entrada baseados em ficheiros atravessam o limite da DLL, nas formas wide e ANSI, juntamente com o accessor do estado de validação; a variante TStream permanece exclusiva do Delphi, porque depende do tempo de vida dos objetos Delphi e de semânticas de referência que não têm uma representação honesta numa ABI C plana. Se as suas ferramentas de recuperação forem um cliente DLL, planeie copiar para um ficheiro temporário e apagá-lo sob os mesmos controlos que aplica à chave
Trate a chave hexadecimal como material de credenciais, com as regras de manuseamento que daria à palavra-passe de um documento, e mantenha o estado de validação no registo que a sua cadeia de custódia produzir, para que um leitor posterior consiga distinguir uma extração verificada de uma extração sem atestação. Se está a avaliar um componente PDF Delphi para perícia, arquivo em massa ou migração de DRM, os pontos de entrada de chave bruta, a garantia só de leitura e a superfície de auditoria da encriptação fazem todos parte da mesma biblioteca, e pode consultar a lista completa de funcionalidades na página do produto PDF Library for Delphi