O PDFlibPas 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 PDFlibPas, 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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
// 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 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 PDFlibPas vai tentar novamente uma senha errada?
O PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas reporta LastErrorCode 401 e para depois da primeira tentativa, porque nenhum número de tentativas de senha conserta um arquivo estruturalmente quebrado
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 PDFlibPas um TFileStream ou TMemoryStream para um documento protegido por senha e não há necessidade de rebobiná-lo entre novas tentativas; o PDFlibPas 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 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;
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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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