PDFlibPas réessaie un mot de passe erroné sur un PDF chiffré en jetant le TPDFDocument qui vient d'échouer et en en créant un tout nouveau pour la tentative suivante, piloté par un callback OnPassword (TPDFlibPasswordEvent) qui s'exécute jusqu'à seize fois avant d'abandonner. C'est un écart délibéré par rapport à l'instinct que la plupart des développeurs Delphi adoptent en premier : garder l'objet document déjà assis en mémoire, lui donner un mot de passe corrigé, et recharger sur place plutôt que de repartir de zéro. La boucle de nouvelle tentative de PDFlibPas, ajoutée en v3.245.0, prend la position opposée, pour des raisons spécifiques à ce qu'une tentative de mot de passe échouée laisse derrière elle. Le scénario derrière cela est assez ordinaire pour que la plupart des applications Delphi lourdes en documents finissent par le rencontrer : un écran d'admission accepte un PDF, un trailer chiffré force une boîte de dialogue de mot de passe, l'opérateur se trompe de doigt sur la chaîne, et la boîte de dialogue réapparaît pour une seconde tentative. Rien dans cette expérience utilisateur n'est inhabituel, si bien que le code derrière doit accepter plus d'un mot de passe candidat pour le même fichier, et il doit le faire en toute sécurité, sans faire fuir l'état de la tentative rejetée dans celle qui suit
Pourquoi ne pouvez-vous pas simplement réessayer sur le même objet document ?
Réutiliser un TPDFDocument à travers les tentatives de mot de passe ne fonctionne pas, car une tentative échouée a déjà démonté cet objet en interne plutôt que de le laisser dans un état en pause, reprenable. Ouvrir un PDF chiffré signifie analyser la table de référence croisée, construire un lecteur par-dessus la source sous-jacente, et construire un gestionnaire de chiffrement à partir de quel que soit le mot de passe fourni, tout cela avant même que PDFlibPas puisse tester si ce mot de passe est correct. Quand le mot de passe s'avère faux, la routine de chargement interne du document nettoie le lecteur, la table de référence croisée, et le gestionnaire de chiffrement dans le cadre de son échec, exactement comme il se doit, ce qui signifie qu'il n'y a pas d'analyseur à moitié construit assis là à attendre un mot de passe corrigé à un second appel. Faites passer ce même objet par une autre tentative de chargement tout de même et le mode d'échec est exactement le genre misérable à déboguer : une erreur fait surface depuis un état interne construit pour une analyse différente, déjà échouée, sans rien qui pointe évidemment vers le mot de passe trois appels en amont. PDFlibPas évite toute cette classe de problème en ne tentant jamais de récupérer un objet document une fois qu'il a échoué à s'ouvrir ; chaque tentative reçoit un document qui n'a jamais vu de mauvais mot de passe, lecteur et table de référence croisée inclus
Comment le callback OnPassword demande-t-il le mot de passe suivant ?
TPDFlibPasswordEvent est le type de callback que PDFlibPas invoque via TPDFlib.LoadFromFile, LoadFromStream, et LoadFromString chaque fois que le mot de passe qui vient d'être essayé s'avère faux, et il transmet trois choses au gestionnaire : quelle tentative est sur le point de s'exécuter, un paramètre Password à écraser avec le prochain candidat, et un drapeau Retry qui vaut faux par défaut
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
Le mot de passe transmis dans l'appel LoadFromFile original compte comme la tentative un, si bien que la première fois qu'OnPassword se déclenche du tout, AttemptNumber arrive à 2. Laissez Retry non défini à un moment donné, ou épuisez les seize tentatives sans mot de passe correct, et LoadFromFile renvoie 0 avec LastErrorCode réglé sur 404, le code de PDFlibPas pour un mot de passe rejeté
À l'intérieur de la boucle de nouvelle tentative : un nouveau TPDFDocument à chaque tentative
En interne, PDFlibPas répond à la question du cycle de vie de l'objet de la même façon pour LoadFromFile, LoadFromStream, et LoadFromString : chaque tentative, y compris la première, construit un TPDFDocument frais, le fait passer par la séquence d'ouverture complète avec quel que soit le mot de passe utilisé pour cette tentative, et ne garde l'objet que si le mot de passe se vérifie. Le TPDFDocument d'une tentative rejetée est libéré immédiatement, emportant avec lui son lecteur, sa table de référence croisée, et son gestionnaire de chiffrement, et la tentative suivante repart avec un objet sans aucun historique
// 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;
Cette ligne Doc := nil juste avant le bloc Finally est tout le contrat de cycle de vie de l'objet en une seule instruction. Un document qui échoue emporte son état d'analyseur à moitié construit dans la tombe avec lui, par conception, et un document qui réussit est le seul jamais ajouté à FDocs, la collection que TPDFlib garde pour chaque document que l'appelant a ouvert. Rien d'une tentative rejetée n'est visible depuis l'extérieur de la boucle de nouvelle tentative : ni un lecteur à moitié initialisé, ni un compte de pages périmé, ni un gestionnaire de chiffrement construit à partir de la mauvaise clé
Combien de fois PDFlibPas réessaiera-t-il un mot de passe erroné ?
PDFlibPas autorise seize tentatives totales contre un seul appel LoadFromFile, LoadFromStream, ou LoadFromString, en comptant le mot de passe transmis dans l'appel lui-même comme la tentative un. OnPassword ne se déclenche jamais que pour les tentatives deux à seize, ce qui plafonne le callback à quinze invocations ; demandez une dix-septième tentative et PDFlibPas refuse sans même invoquer le gestionnaire. Laissez Retry à sa valeur par défaut de faux à un moment quelconque, ou épuisez les seize tentatives sans mot de passe correct, et LoadFromFile renvoie 0 avec LastErrorCode réglé sur 404, le code de PDFlibPas pour un mot de passe rejeté. Le plafond existe pour des raisons au-delà de l'ordre : une boucle de nouvelle tentative sans limite est un moyen facile de transformer un mot de passe mal tapé en un déni de service accidentel contre quel que soit le thread exécutant le chargement, surtout une fois qu'un gestionnaire est câblé à quelque chose d'automatisé, comme une liste de mots de passe précédemment vus, plutôt qu'un humain cliquant à travers une boîte de dialogue. PDFlibPas honore aussi Abort appelé sur l'instance TPDFlib depuis l'intérieur du gestionnaire, puisque Sender arrive comme ce même objet, utile derrière un bouton Annuler sur une boîte de dialogue de mot de passe, et arrête la boucle de nouvelle tentative à la prochaine vérification quelle que soit la valeur à laquelle Retry a été réglé. Un chargement qui échoue pour une raison autre qu'un mot de passe erroné, une table de référence croisée endommagée par exemple, n'entre jamais du tout dans la boucle de nouvelle tentative : PDFlibPas rapporte LastErrorCode 401 et s'arrête après la première tentative, car aucun nombre de suppositions de mot de passe ne corrige un fichier structurellement cassé
La boucle de nouvelle tentative fonctionne-t-elle de la même façon pour les fichiers, les flux, et les chaînes ?
Le callback OnPassword et le plafond de seize tentatives se comportent de façon identique à travers LoadFromFile, LoadFromStream, et LoadFromString, bien que les trois points d'entrée conservent leur source différemment entre les tentatives. Un chemin de fichier est bon marché à revisiter, puisque chaque tentative rouvre simplement le fichier nommé, et une source de type chaîne se trouve déjà en mémoire comme la propre copie de l'appelant, si bien qu'aucune des deux n'a besoin d'aide de la part de l'appelant entre les essais. Un flux fourni par l'appelant est le seul cas qui mérite qu'on s'y arrête : LoadFromStream repositionne ce flux à la position zéro et le copie en interne avant la première tentative d'analyse, si bien que chaque tentative suivante, et le TPDFDocument fraîchement construit derrière elle, rejoue depuis cette copie interne plutôt que depuis quelle que soit la position où une analyse échouée a laissé le flux. Donnez à PDFlibPas un TFileStream ou un TMemoryStream pour un document protégé par mot de passe et il n'y a pas besoin de le rembobiner entre les tentatives ; PDFlibPas tient déjà compte d'une position qu'une première tentative échouée a pu déplacer
Intégrer la nouvelle tentative de mot de passe dans un écran d'admission de document
Un flux de travail d'admission de document est le foyer naturel de ce callback, car c'est exactement la forme de problème qu'OnPassword a été construit pour résoudre : un fichier arrive de l'extérieur de l'application, son mot de passe n'est pas connu avec certitude à l'avance, et la personne fournissant des candidats a besoin de plus d'une supposition sans que le code environnant n'écrive sa propre boucle de nouvelle tentative autour 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 ne reçoit Lib qu'une fois que LoadFromFile a renvoyé 1, ce qui signifie qu'un mot de passe dans cet échange s'est réellement vérifié contre le gestionnaire de chiffrement du fichier ; une tentative rejetée n'atteint jamais cette ligne, et un document à moitié ouvert non plus. Ce qui vient ensuite, une fois qu'un document comme celui-ci est confirmé ouvert, mérite un second regard sur ses réglages de protection plutôt qu'une hypothèse selon laquelle le mot de passe qui a fonctionné est toute l'histoire de sécurité : auditer ce que le dictionnaire /Encrypt d'un document déclare réellement couvre la lecture de l'algorithme, de la révision, et des bits de permission que PDFlibPas expose une fois qu'un fichier comme celui-ci se charge
La nouvelle tentative de mot de passe est aussi une instance étroite d'une discipline plus large que PDFlibPas applique dans toute sa couche d'analyse : un fichier qui n'a pas encore fait ses preuves ne bénéficie d'aucun doute, que la question soit quel mot de passe le déverrouille ou si un champ de longueur à l'intérieur ment sur la taille de tampon dont il a besoin. Durcir un analyseur PDF Pascal contre des fichiers malveillants couvre l'autre moitié de cette discipline, les décodeurs qui traitent chaque programme de police et flux d'image dans un PDF entrant comme une entrée adversariale plutôt qu'un document bien formé qui a simplement oublié son mot de passe
OnPassword et la boucle de nouvelle tentative derrière lui font partie de la bibliothèque PDF PDFlibPas pour Delphi et C++Builder standard, disponible partout où LoadFromFile, LoadFromStream, ou LoadFromString le sont déjà, sans module séparé ni palier de licence requis pour un document qui a simplement besoin d'une seconde supposition sur son mot de passe