Artigo Técnico

Tentando Novamente Senhas de PDF Criptografado em Delphi com o PDF Library for Delphi

O PDF Library for Delphi tenta novamente uma senha errada em um PDF criptografado descartando o TPDFDocument que acabou de falhar e criando um totalmente novo para a próxima tentativa, conduzido por um callback OnPassword (TPDFlibPasswordEvent) que roda até dezesseis vezes antes de desistir. Esse é um desvio deliberado do instinto que a maioria dos desenvolvedores Delphi busca primeiro: manter o objeto de documento já sentado em memória, alimentá-lo com uma senha corrigida, e carregar novamente no lugar, em vez de começar do zero. O loop de nova tentativa do PDF Library for Delphi, adicionado na v3.245.0, toma a posição oposta, por motivos específicos ao que uma tentativa de senha falha deixa para trás. O cenário por trás disso é comum o bastante que a maioria das aplicações Delphi pesadas em documentos acaba encontrando: uma tela de admissão aceita um PDF, um trailer criptografado força um diálogo de senha, o operador erra a digitação, e o diálogo reaparece para uma segunda tentativa. Nada nessa experiência de usuário é incomum, então o código por trás dela precisa aceitar mais de uma senha candidata para o mesmo arquivo, e precisa fazer isso com segurança, sem vazar estado da tentativa rejeitada para a que se segue

Por que você não pode simplesmente tentar novamente no mesmo objeto de documento?

Reutilizar um TPDFDocument entre tentativas de senha não funciona, porque uma tentativa falha já desmontou esse objeto internamente, em vez de deixá-lo em algum estado pausado e retomável. Abrir um PDF criptografado significa analisar a tabela de referência cruzada, construir um leitor sobre a origem subjacente, e construir um manipulador de criptografia a partir de qualquer senha que foi fornecida, tudo antes de o PDF Library for Delphi sequer poder testar se essa senha está correta. Quando a senha se revela errada, a rotina de carregamento interna do documento limpa o leitor, a tabela de referência cruzada, e o manipulador de criptografia como parte de falhar, exatamente como deveria, o que significa que não há nenhum analisador meio construído sentado ali esperando por uma senha corrigida em uma segunda chamada. Conduza esse mesmo objeto por outra tentativa de carregamento de qualquer forma e o modo de falha é exatamente do tipo miserável de depurar: um erro surge de estado interno construído para uma análise diferente, já falha, sem nada nele obviamente apontando de volta para a senha três chamadas rio acima. O PDF Library for Delphi evita toda a classe de problema nunca tentando recuperar um objeto de documento uma vez que falhou ao abrir; toda tentativa recebe um documento que nunca viu uma senha errada, leitor e tabela de referência cruzada incluídos

Como o callback OnPassword pede a próxima senha?

TPDFlibPasswordEvent é o tipo de callback que o PDF Library for Delphi invoca por meio de TPDFlib.LoadFromFile, LoadFromStream, e LoadFromString sempre que a senha que acabou de ser tentada se revela errada, e entrega ao handler três coisas: qual tentativa está prestes a rodar, um parâmetro Password para sobrescrever com o próximo candidato, e uma flag Retry que assume false por padrão

TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean) of object;

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

A senha passada na chamada original LoadFromFile conta como tentativa um, então a primeira vez que OnPassword dispara de fato, AttemptNumber chega como 2. Deixe Retry sem definir e o carregamento falha limpo com LastErrorCode 404; defina-o como true e o PDF Library for Delphi tenta novamente com o que quer que o handler tenha acabado de escrever em Password

Dentro do loop de nova tentativa: um TPDFDocument novo para cada tentativa

Internamente, o PDF Library for Delphi responde à questão de ciclo de vida do objeto da mesma forma para LoadFromFile, LoadFromStream, e LoadFromString: toda tentativa, incluindo a primeira, constrói um TPDFDocument novo, o executa através da sequência de abertura completa com a senha que aquela tentativa está usando, e só mantém o objeto se a senha se verificar. O TPDFDocument de uma tentativa rejeitada é liberado imediatamente, levando junto seu leitor, tabela de referência cruzada, e manipulador de criptografia, e a próxima tentativa começa do zero com um objeto que não tem nenhum histórico de forma alguma

Fluxograma do PDF Library for Delphi do loop de repetição de senha em que cada tentativa constrói um TPDFDocument novo, uma tentativa rejeitada libera seu reader, tabela de referências cruzadas e manipulador de criptografia, e o callback OnPassword decide se outra tentativa roda ou o carregamento falha com LastErrorCode 404
Um candidato que falha nunca sobrevive ao loop: seu documento é liberado com o estado do parser ainda meio construído. Definir Retry grava o próximo candidato em Password e repete com um objeto que nunca viu uma senha errada
// Trecho simplificado de dentro de LoadFromFile: toda tentativa recebe um
// documento que nunca viu uma senha rejeitada anteriormente. FileName,
// AttemptNumber e AttemptPassword vêm do método que o contém.
Var
  Doc: TPDFDocument;
  LoadResult: TPLLoadResult;
  Success: Boolean;
Begin
  Success := False;
  Repeat
    Doc := TPDFDocument.Create;
    Doc.DecodeMode := FDefaultDecodeMode;
    Try
      LoadResult := Doc.LoadFromFile(FileName, AttemptPassword);
      Success := LoadResult = lrOkay;
      if Success then
      begin
        FDocs.Add(Doc);            // entrega o documento verificado à coleção
        Doc := nil;                 // do chamador; pula o Free abaixo
      end;
    Finally
      Doc.Free;                     // o leitor, a tabela de referência cruzada
    End;                            // e o manipulador de criptografia de uma tentativa rejeitada são desmontados bem aqui
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // sucesso, ou uma falha que não é de senha: para
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Essa linha Doc := nil logo antes do bloco Finally é o contrato de ciclo de vida do objeto inteiro em uma instrução. Um documento que falha carrega seu estado de analisador meio construído para a sepultura junto com ele, por design, e um documento que tem sucesso é o único jamais adicionado a FDocs, a coleção que TPDFlib mantém para cada documento que o chamador tem aberto. Nada de uma tentativa rejeitada é visível de fora do loop de nova tentativa: nenhum leitor meio inicializado, nenhuma contagem de página obsoleta, nenhum manipulador de criptografia construído a partir da chave errada

Quantas vezes o PDF Library for Delphi vai tentar novamente uma senha errada?

O PDF Library for Delphi permite dezesseis tentativas totais contra uma única chamada LoadFromFile, LoadFromStream, ou LoadFromString, contando a senha passada na própria chamada como tentativa um. OnPassword só dispara para as tentativas de dois a dezesseis, o que limita o callback a quinze invocações; peça uma décima sétima tentativa e o PDF Library for Delphi recusa sem sequer invocar o handler. Deixe Retry em seu padrão de false em qualquer ponto, ou esgote todas as dezesseis tentativas sem uma senha correta, e LoadFromFile retorna 0 com LastErrorCode definido para 404, o código do PDF Library for Delphi para uma senha rejeitada. O teto existe por motivos além de arrumação: um loop de nova tentativa sem limite é uma forma fácil de transformar uma senha digitada errada em uma negação de serviço acidental contra qualquer thread que esteja rodando o carregamento, especialmente uma vez que um handler é conectado a algo automatizado, como uma lista de senhas vistas anteriormente, em vez de um humano clicando por um diálogo. O PDF Library for Delphi também respeita Abort chamado na instância TPDFlib de dentro do handler, já que Sender chega como esse mesmo objeto, útil atrás de um botão Cancelar em um diálogo de senha, e para o loop de nova tentativa na próxima verificação independentemente do que Retry foi definido. Um carregamento que falha por um motivo diferente de uma senha errada, uma tabela de referência cruzada danificada, por exemplo, nunca entra no loop de nova tentativa de forma alguma: o PDF Library for Delphi reporta LastErrorCode 401 e para depois da primeira tentativa, porque nenhum número de tentativas de senha conserta um arquivo estruturalmente quebrado

PDF Library for Delphi: trilha de tentativas de um a dezesseis mostrando OnPassword disparando a partir da tentativa dois, a tentativa dezessete recusada sem invocar o handler, e os resultados separados para sucesso, LastErrorCode 404 e LastErrorCode 401
Somente a senha passada à própria chamada de carregamento conta como tentativa um, e o callback nunca roda mais de quinze vezes por carregamento. Um arquivo estruturalmente corrompido ignora o loop por completo e reporta 401 após uma única tentativa

O loop de nova tentativa funciona da mesma forma para arquivos, streams e strings?

O callback OnPassword e o teto de dezesseis tentativas se comportam identicamente através de LoadFromFile, LoadFromStream, e LoadFromString, embora os três pontos de entrada segurem sua origem de forma diferente entre tentativas. Um caminho de arquivo é barato de revisitar, já que cada tentativa simplesmente reabre o arquivo nomeado, e uma origem de string já senta em memória como a própria cópia de quem chama, então nenhuma das duas precisa de nenhuma ajuda de quem chama entre tentativas. Um stream fornecido por quem chama é o único caso que vale a pena parar para considerar: LoadFromStream busca esse stream de volta para a posição zero e o copia internamente antes da primeira tentativa de análise, de modo que toda tentativa subsequente, e o TPDFDocument recém-construído por trás dela, reproduz a partir dessa cópia interna, em vez de de onde quer que uma análise falha tenha deixado a posição do stream. Entregue ao PDF Library for Delphi um TFileStream ou TMemoryStream para um documento protegido por senha e não há necessidade de rebobiná-lo entre novas tentativas; o PDF Library for Delphi já considera uma posição que uma primeira tentativa falha possa ter movido

Encaixando a nova tentativa de senha em uma tela de admissão de documento

Um fluxo de trabalho de admissão de documento é o lar natural para esse callback, porque é exatamente a forma de problema para a qual OnPassword foi construído: um arquivo chega de fora da aplicação, sua senha não é conhecida com certeza de antemão, e a pessoa fornecendo candidatos precisa de mais de um palpite sem o código ao redor escrever seu próprio loop de nova tentativa em torno de LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber conta a partir de 2: a senha já tentada foi a tentativa 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry é False quando o operador cancela, o que deixa
  // LastErrorCode em 404 para o chamador reportar.
end;
procedure TIntakeForm.LoadInboundDocument;
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.OnPassword := SupplyPassword;
    if Lib.LoadFromFile('inbound-invoice.pdf', '') = 1 then
      RegisterIntakeDocument(Lib)        // só um documento verificado chega aqui
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument só recebe Lib uma vez que LoadFromFile retornou 1, significando que alguma senha naquela troca de fato se verificou contra o manipulador de criptografia do arquivo; uma tentativa rejeitada nunca alcança essa linha, e nem um documento meio aberto. O que vem em seguida, uma vez que um documento assim é confirmado como aberto, vale a pena um segundo olhar em suas configurações de proteção, em vez de uma suposição de que a senha que funcionou é toda a história de segurança: auditando o que o dicionário /Encrypt de um documento de fato declara cobre ler o algoritmo, revisão, e bits de permissão que o PDF Library for Delphi expõe uma vez que um arquivo assim carrega

A nova tentativa de senha também é uma instância restrita de uma disciplina mais ampla que o PDF Library for Delphi aplica em toda sua camada de análise: um arquivo que ainda não se provou não recebe nenhum benefício da dúvida, seja a questão qual senha o destranca ou se um campo de comprimento dentro dele está mentindo sobre o tamanho de buffer que precisa. Reforçando um analisador de PDF Pascal contra arquivos maliciosos cobre a outra metade dessa disciplina, os decodificadores que tratam todo programa de fonte e stream de imagem em um PDF de entrada como entrada adversarial, em vez de um documento bem formado que meramente esqueceu sua senha

OnPassword e o loop de nova tentativa por trás dele fazem parte da biblioteca PDF Library for Delphi para Delphi e C++Builder padrão, disponível em qualquer lugar onde LoadFromFile, LoadFromStream, ou LoadFromString já estejam, sem nenhum módulo separado ou nível de licença exigido para um documento que só precisa de um segundo palpite para sua senha