O PDFlibPas 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 PDFlibPas, 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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
// Simplified excerpt from inside LoadFromFile: every attempt gets a
// document that has never seen a previously rejected password. FileName,
// AttemptNumber and AttemptPassword come from the enclosing method.
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); // hand the verified document to the
Doc := nil; // caller's collection; skip the Free below
end;
Finally
Doc.Free; // a rejected attempt's reader, xref table
End; // and crypt handler are torn down right here
if Success or (LoadResult <> lrWrongPassword) then
Break; // success, or a non-password failure: stop
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 PDFlibPas tentar de novo uma palavra-passe errada?
O PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas reporta LastErrorCode 401 e para depois da primeira tentativa, porque nenhum número de tentativas de palavra-passe corrige um ficheiro estruturalmente danificado
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 PDFlibPas uma TFileStream ou TMemoryStream para um documento protegido por palavra-passe, não há necessidade de a rebobinar entre tentativas; o PDFlibPas 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 counts from 2: the password already tried was attempt 1.
Typed := '';
Retry := InputQuery('Password required',
Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
if Retry then
Password := Typed;
// Retry is False when the operator cancels, which leaves
// LastErrorCode at 404 for the caller to report.
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) // only a verified document reaches here
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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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