Artigo Técnico

Tentar Novamente Palavras-Passe de PDF Encriptado em Delphi com o PDF Library for Delphi

O PDF Library for Delphi volta a tentar uma palavra-passe errada num PDF encriptado descartando o TPDFDocument que acabou de falhar e criando um inteiramente novo para a tentativa seguinte, orientado por um callback OnPassword (TPDFlibPasswordEvent) que corre até dezasseis tentativas antes de desistir. Este é um afastamento deliberado do instinto a que a maioria dos programadores Delphi recorre primeiro: manter o objeto de documento já em memória, alimentá-lo com uma palavra-passe corrigida, e voltar a carregar no mesmo lugar em vez de recomeçar do zero. O ciclo de repetição do PDF Library for Delphi, acrescentado na v3.245.0, toma a posição oposta, por razões específicas ao que uma tentativa de palavra-passe falhada deixa para trás. O cenário por trás disto é suficientemente comum para que a maioria das aplicações Delphi que lidam intensamente com documentos acabe por o encontrar: um ecrã de admissão aceita um PDF, um trailer encriptado força uma caixa de diálogo de palavra-passe, o operador engana-se ao escrever a cadeia de texto, e a caixa de diálogo reaparece para uma segunda tentativa. Nada nessa experiência de utilizador é invulgar, pelo que o código por trás dela tem de aceitar mais do que uma palavra-passe candidata para o mesmo ficheiro, e tem de o fazer em segurança, sem deixar o estado da tentativa rejeitada infiltrar-se na que se segue

Porque é que não se pode simplesmente tentar de novo sobre o mesmo objeto de documento?

Reutilizar um TPDFDocument entre tentativas de palavra-passe não funciona, porque uma tentativa falhada já desmontou esse objeto internamente, em vez de o deixar num estado pausado e retomável. Abrir um PDF encriptado significa interpretar a tabela de referência cruzada, construir um leitor sobre a fonte subjacente, e construir um manipulador de cifra a partir de qualquer que seja a palavra-passe fornecida, tudo isto antes de o PDF Library for Delphi sequer conseguir testar se essa palavra-passe está correta. Quando a palavra-passe se revela errada, a rotina de carregamento interna do documento limpa o leitor, a tabela de referência cruzada, e o manipulador de cifra como parte da falha, exatamente como deveria, o que significa que não fica lá nenhum interpretador meio construído à espera de uma palavra-passe corrigida numa segunda chamada. Conduzir esse mesmo objeto por outra tentativa de carregamento de qualquer forma resulta exatamente no tipo de falha miserável de depurar: um erro surge a partir de estado interno construído para uma interpretação diferente, já falhada, sem nada nele a apontar de forma óbvia de volta para a palavra-passe três chamadas a montante. O PDF Library for Delphi evita toda essa classe de problema nunca tentando recuperar um objeto de documento depois de este falhar ao abrir; cada tentativa recebe um documento que nunca viu uma palavra-passe errada, leitor e tabela de referência cruzada incluídos

Como é que o callback OnPassword pede a palavra-passe seguinte?

O TPDFlibPasswordEvent é o tipo de callback que o PDF Library for Delphi invoca através de TPDFlib.LoadFromFile, LoadFromStream, e LoadFromString sempre que a palavra-passe acabada de tentar se revela errada, e entrega ao manipulador três coisas: qual a tentativa prestes a correr, um parâmetro Password para sobrescrever com o próximo candidato, e uma flag Retry que assume false por predefinição

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

A palavra-passe passada na chamada original a LoadFromFile conta como a tentativa um, pelo que, na primeira vez que OnPassword dispara, AttemptNumber chega como 2. Se se deixar Retry por definir na predefinição de false, o carregamento falha de forma limpa com LastErrorCode 404; se se definir como true, o PDF Library for Delphi tenta de novo com o que quer que o manipulador acabou de escrever em Password

Dentro do ciclo de repetição: um TPDFDocument novo para cada tentativa

Internamente, o PDF Library for Delphi responde à questão do ciclo de vida do objeto da mesma forma para LoadFromFile, LoadFromStream, e LoadFromString: cada tentativa, incluindo a primeira, constrói um TPDFDocument novo, corre-o através da sequência completa de abertura com qualquer que seja a palavra-passe que essa tentativa está a usar, e só mantém o objeto se a palavra-passe se verificar. O TPDFDocument de uma tentativa rejeitada é libertado de imediato, levando consigo o seu leitor, a tabela de referência cruzada, e o manipulador de cifra, e a tentativa seguinte recomeça com um objeto sem qualquer histórico

Fluxograma da PDF Library for Delphi do ciclo de repetição da palavra-passe em que cada tentativa constrói um TPDFDocument novo, uma tentativa rejeitada liberta o seu leitor, tabela de referências cruzadas e handler criptográfico, e o callback OnPassword decide se corre outra tentativa ou se o carregamento falha com LastErrorCode 404
Um candidato falhado nunca sobrevive ao ciclo: o seu documento é libertado com o estado do analisador ainda meio construído. Definir Retry escreve o próximo candidato em Password e repete com um objeto que nunca viu uma palavra-passe errada
// Excerto simplificado do interior de LoadFromFile: cada tentativa recebe um
// documento que nunca viu uma palavra-passe rejeitada anteriormente. FileName,
// AttemptNumber e AttemptPassword vêm do método envolvente.
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; salta o Free abaixo
      end;
    Finally
      Doc.Free;                     // o leitor, a tabela xref e o manipulador
    End;                            // de cifra de uma tentativa rejeitada são destruídos aqui mesmo
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // sucesso, ou uma falha não relacionada com a palavra-passe: pára
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Essa linha Doc := nil mesmo antes do bloco Finally é todo o contrato de ciclo de vida do objeto numa única instrução. Um documento que falha leva o seu estado de interpretador meio construído para o túmulo consigo, por conceção, e um documento que tem sucesso é o único alguma vez acrescentado a FDocs, a coleção que o TPDFlib mantém para cada documento que o chamador tenha aberto. Nada de uma tentativa rejeitada é visível de fora do ciclo de repetição: nem um leitor meio inicializado, nem uma contagem de páginas obsoleta, nem um manipulador de cifra construído a partir da chave errada

Quantas vezes vai o PDF Library for Delphi tentar de novo uma palavra-passe errada?

O PDF Library for Delphi permite dezasseis tentativas totais contra uma única chamada a LoadFromFile, LoadFromStream, ou LoadFromString, contando a palavra-passe passada na própria chamada como a tentativa um. O OnPassword só dispara para as tentativas dois a dezasseis, o que limita o callback a quinze invocações; peça-se uma décima sétima tentativa e o PDF Library for Delphi recusa sem sequer invocar o manipulador. Se se deixar Retry na sua predefinição de false em qualquer ponto, ou se se esgotarem todas as dezasseis tentativas sem uma palavra-passe correta, LoadFromFile devolve 0 com LastErrorCode definido para 404, o código do PDF Library for Delphi para uma palavra-passe rejeitada. O limite existe por razões além da arrumação: um ciclo de repetição ilimitado é uma forma fácil de transformar uma palavra-passe mal escrita numa negação de serviço acidental contra o que quer que seja a thread a correr o carregamento, especialmente assim que um manipulador for ligado a algo automatizado, como uma lista de palavras-passe anteriormente vistas, em vez de um humano a clicar numa caixa de diálogo. O PDF Library for Delphi também respeita o Abort chamado sobre a instância de TPDFlib de dentro do manipulador, já que Sender chega como esse mesmo objeto, útil por trás de um botão Cancelar numa caixa de diálogo de palavra-passe, e para o ciclo de repetição na verificação seguinte independentemente do que Retry tenha sido definido. Um carregamento que falhe por uma razão diferente de uma palavra-passe errada, uma tabela de referência cruzada danificada por exemplo, nunca entra de todo no ciclo de repetição: o PDF Library for Delphi reporta LastErrorCode 401 e para depois da primeira tentativa, porque nenhum número de tentativas de palavra-passe corrige um ficheiro estruturalmente danificado

PDF Library for Delphi: Percurso de tentativas de um a dezasseis mostrando OnPassword a disparar a partir da segunda tentativa, a décima sétima tentativa recusada sem invocar o handler, e os resultados separados para sucesso, LastErrorCode 404 e LastErrorCode 401
Apenas a palavra-passe entregue à própria chamada de carregamento conta como tentativa um, e o callback nunca corre mais de quinze vezes por carregamento. Um ficheiro estruturalmente danificado ignora totalmente o ciclo e reporta 401 após uma única tentativa

O ciclo de repetição funciona da mesma forma para ficheiros, streams, e cadeias de texto?

O callback OnPassword e o limite de dezasseis tentativas comportam-se de forma idêntica em LoadFromFile, LoadFromStream, e LoadFromString, embora os três pontos de entrada guardem a sua origem de forma diferente entre tentativas. Um caminho de ficheiro é barato de revisitar, já que cada tentativa simplesmente reabre o ficheiro nomeado, e uma origem de cadeia de texto já está em memória como a própria cópia do chamador, pelo que nenhuma das duas precisa de qualquer ajuda do chamador entre tentativas. Uma stream fornecida pelo chamador é o único caso que vale a pena analisar com mais atenção: o LoadFromStream reposiciona essa stream de volta à posição zero e copia-a internamente antes da primeira tentativa de interpretação, pelo que cada tentativa subsequente, e o TPDFDocument recém-construído por trás dela, reproduz a partir dessa cópia interna em vez de a partir de onde quer que uma interpretação falhada tenha deixado a posição da stream. Ao entregar ao PDF Library for Delphi uma TFileStream ou TMemoryStream para um documento protegido por palavra-passe, não há necessidade de a rebobinar entre tentativas; o PDF Library for Delphi já contabiliza uma posição que uma primeira tentativa falhada possa ter deslocado

Encaixar a repetição de palavra-passe num ecrã de admissão de documentos

Um fluxo de trabalho de admissão de documentos é o lugar natural para este callback, porque é exatamente a forma de problema para que o OnPassword foi construído: um ficheiro chega de fora da aplicação, a sua palavra-passe não é conhecida com certeza antecipadamente, e quem fornece candidatos precisa de mais do que uma tentativa sem que o código envolvente escreva o seu próprio ciclo de repetição à volta 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 palavra-passe 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;

O RegisterIntakeDocument só recebe Lib depois de LoadFromFile ter devolvido 1, o que significa que alguma palavra-passe nessa troca de facto se verificou contra o manipulador de cifra do ficheiro; uma tentativa rejeitada nunca chega a essa linha, nem tão-pouco um documento meio aberto. O que se segue, assim que um documento como este é confirmado como aberto, vale a pena um segundo olhar sobre as suas definições de proteção, em vez da suposição de que a palavra-passe que funcionou é toda a história de segurança: auditar o que o dicionário /Encrypt de um documento efetivamente declara aborda a leitura do algoritmo, revisão, e bits de permissão que o PDF Library for Delphi expõe assim que um ficheiro como este carrega

A repetição de palavra-passe é também uma instância restrita de uma disciplina mais ampla que o PDF Library for Delphi aplica ao longo de toda a sua camada de interpretação: um ficheiro que ainda não se provou a si próprio não recebe o benefício da dúvida, seja a questão qual a palavra-passe que o desbloqueia ou se um campo de comprimento lá dentro está a mentir sobre o tamanho de buffer de que precisa. Reforçar um interpretador de PDF em Pascal contra ficheiros maliciosos aborda a outra metade dessa disciplina, os descodificadores que tratam cada programa de tipo de letra e stream de imagem num PDF de entrada como entrada adversarial, em vez de um documento bem formado que apenas se esqueceu da sua palavra-passe

O OnPassword e o ciclo de repetição por trás dele fazem parte da biblioteca de PDF PDF Library for Delphi padrão para Delphi e C++Builder, disponível em qualquer sítio onde LoadFromFile, LoadFromStream, ou LoadFromString já estejam, sem qualquer módulo separado ou nível de licença adicional necessário para um documento que apenas precisa de uma segunda tentativa à sua palavra-passe