PDFlibPas versucht ein falsches Passwort auf einem verschlüsselten PDF erneut, indem es das gerade gescheiterte TPDFDocument verwirft und für den nächsten Versuch ein vollkommen neues erzeugt, angetrieben von einem OnPassword-Callback (TPDFlibPasswordEvent), der bis zu sechzehn Versuche lang läuft, bevor er aufgibt. Das ist eine bewusste Abweichung vom Instinkt, zu dem die meisten Delphi-Entwickler zuerst greifen würden: das bereits im Speicher sitzende Dokumentobjekt behalten, ihm ein korrigiertes Passwort füttern und an Ort und Stelle erneut laden, statt von Grund auf neu zu beginnen. PDFlibPas' Retry-Schleife, hinzugefügt in v3.245.0, nimmt die entgegengesetzte Position ein, aus Gründen, die spezifisch dafür sind, was ein fehlgeschlagener Passwortversuch hinterlässt. Das Szenario dahinter ist gewöhnlich genug, dass die meisten dokumentenlastigen Delphi-Anwendungen früher oder später darauf treffen: Ein Aufnahmebildschirm akzeptiert ein PDF, ein verschlüsselter Trailer erzwingt einen Passwort-Dialog, der Bediener vertippt sich beim String, und der Dialog erscheint für einen zweiten Versuch erneut. Nichts an diesem Nutzererlebnis ist ungewöhnlich, sodass der Code dahinter mehr als ein Kandidaten-Passwort für dieselbe Datei akzeptieren muss, und er muss das sicher tun, ohne Zustand aus dem abgelehnten Versuch in den folgenden auslaufen zu lassen
Warum kann man nicht einfach auf demselben Dokumentobjekt erneut versuchen?
Ein TPDFDocument über Passwortversuche hinweg wiederzuverwenden funktioniert nicht, weil ein fehlgeschlagener Versuch dieses Objekt intern bereits abgebaut hat, statt es in irgendeinem pausierten, wiederaufnehmbaren Zustand zu belassen. Ein verschlüsseltes PDF zu öffnen bedeutet, die Kreuzreferenz-Tabelle zu parsen, einen Reader über die zugrunde liegende Quelle zu bauen, und einen Crypt-Handler aus welchem Passwort auch immer übergeben wurde zu konstruieren, alles bevor PDFlibPas überhaupt testen kann, ob dieses Passwort korrekt ist. Stellt sich das Passwort als falsch heraus, räumt die interne Laderoutine des Dokuments den Reader, die Kreuzreferenz-Tabelle und den Crypt-Handler als Teil des Scheiterns auf, genau wie es sollte, was bedeutet, dass kein halb aufgebauter Parser dort sitzt und auf ein korrigiertes Passwort bei einem zweiten Aufruf wartet. Dasselbe Objekt trotzdem durch einen weiteren Ladeversuch zu treiben erzeugt genau die Art von Fehlermodus, der elend zu debuggen ist: Ein Fehler taucht aus internem Zustand auf, der für einen anderen, bereits gescheiterten Parse-Vorgang gebaut wurde, ohne dass irgendetwas daran offensichtlich auf das Passwort drei Aufrufe stromaufwärts zurückzeigt. PDFlibPas vermeidet die gesamte Problemklasse, indem es nie versucht, ein Dokumentobjekt wiederherzustellen, sobald es beim Öffnen gescheitert ist; jeder Versuch bekommt ein Dokument, das nie ein falsches Passwort gesehen hat, Reader und Kreuzreferenz-Tabelle eingeschlossen
Wie fragt der OnPassword-Callback nach dem nächsten Passwort?
TPDFlibPasswordEvent ist der Callback-Typ, den PDFlibPas über TPDFlib.LoadFromFile, LoadFromStream und LoadFromString aufruft, wann immer sich das gerade versuchte Passwort als falsch herausstellt, und er übergibt dem Handler drei Dinge: welcher Versuch gleich läuft, einen Password-Parameter zum Überschreiben mit dem nächsten Kandidaten, und ein Retry-Flag, das standardmäßig false ist
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
Das in den ursprünglichen LoadFromFile-Aufruf übergebene Passwort zählt als Versuch eins, sodass beim allerersten Feuern von OnPassword AttemptNumber als 2 ankommt. Lässt man Retry ungesetzt, scheitert das Laden sauber mit LastErrorCode 404; setzt man es auf true, versucht PDFlibPas es erneut mit dem, was der Handler gerade in Password geschrieben hat
Innerhalb der Retry-Schleife: ein neues TPDFDocument für jeden Versuch
Intern beantwortet PDFlibPas die Objekt-Lebenszyklus-Frage für LoadFromFile, LoadFromStream und LoadFromString gleich: Jeder Versuch, einschließlich des ersten, konstruiert ein frisches TPDFDocument, führt es durch die vollständige Öffnungssequenz mit welchem Passwort auch immer dieser Versuch verwendet, und behält das Objekt nur, falls das Passwort verifiziert. Das TPDFDocument eines abgelehnten Versuchs wird sofort freigegeben, wobei sein Reader, seine Kreuzreferenz-Tabelle und sein Crypt-Handler mit ihm untergehen, und der nächste Versuch beginnt neu mit einem Objekt ganz ohne Geschichte
// 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;
Diese Zeile Doc := nil kurz vor dem Finally-Block ist der gesamte Objekt-Lebenszyklus-Vertrag in einer Anweisung. Ein scheiterndes Dokument nimmt seinen halb aufgebauten Parser-Zustand mit ins Grab, per Design, und ein erfolgreiches Dokument ist das einzige, das je zu FDocs hinzugefügt wird, der Sammlung, die TPDFlib für jedes vom Aufrufer geöffnete Dokument führt. Nichts an einem abgelehnten Versuch ist von außerhalb der Retry-Schleife sichtbar: kein halb initialisierter Reader, keine veraltete Seitenanzahl, kein aus dem falschen Schlüssel gebauter Crypt-Handler
Wie oft versucht PDFlibPas ein falsches Passwort erneut?
PDFlibPas erlaubt sechzehn Gesamtversuche gegen einen einzelnen LoadFromFile-, LoadFromStream- oder LoadFromString-Aufruf, wobei das in den Aufruf selbst übergebene Passwort als Versuch eins zählt. OnPassword feuert nur für die Versuche zwei bis sechzehn, was den Callback auf fünfzehn Aufrufe begrenzt; bittet man um einen siebzehnten Versuch, lehnt PDFlibPas ab, ohne den Handler auch nur aufzurufen. Lässt man Retry an irgendeinem Punkt bei seinem Standard false, oder erschöpft alle sechzehn Versuche ohne korrektes Passwort, gibt LoadFromFile 0 zurück, mit auf 404 gesetztem LastErrorCode, PDFlibPas' Code für ein abgelehntes Passwort. Die Obergrenze existiert aus Gründen über die bloße Ordentlichkeit hinaus: Eine unbegrenzte Retry-Schleife ist ein leichter Weg, ein einziges vertipptes Passwort in einen versehentlichen Denial-of-Service gegen welchen Thread auch immer den Ladevorgang ausführt zu verwandeln, besonders sobald ein Handler an etwas Automatisiertes angeschlossen ist, wie eine Liste zuvor gesehener Passwörter, statt an einen Menschen, der sich durch einen Dialog klickt. PDFlibPas respektiert auch ein auf der TPDFlib-Instanz von innerhalb des Handlers aufgerufenes Abort, da Sender als dasselbe Objekt ankommt, nützlich hinter einem Abbrechen-Button auf einem Passwort-Dialog, und stoppt die Retry-Schleife bei der nächsten Prüfung, unabhängig davon, worauf Retry gesetzt war. Ein Ladevorgang, der aus einem anderen Grund als einem falschen Passwort scheitert, etwa einer beschädigten Kreuzreferenz-Tabelle, betritt die Retry-Schleife überhaupt nie: PDFlibPas meldet LastErrorCode 401 und stoppt nach dem ersten Versuch, weil keine Anzahl an Passwort-Rateversuchen eine strukturell defekte Datei repariert
Funktioniert die Retry-Schleife für Dateien, Streams und Strings gleich?
Der OnPassword-Callback und die Sechzehn-Versuche-Obergrenze verhalten sich über LoadFromFile, LoadFromStream und LoadFromString identisch, obwohl die drei Einstiegspunkte ihre Quelle zwischen Versuchen unterschiedlich festhalten. Ein Dateipfad ist billig erneut zu besuchen, da jeder Versuch einfach die benannte Datei erneut öffnet, und eine String-Quelle sitzt bereits als eigene Kopie des Aufrufers im Speicher, sodass keine von beiden Hilfe vom Aufrufer zwischen Versuchen braucht. Ein vom Aufrufer bereitgestellter Stream ist der eine Fall, bei dem es sich lohnt innezuhalten: LoadFromStream setzt diesen Stream vor dem ersten Parse-Versuch auf Position null zurück und kopiert ihn intern, sodass jeder nachfolgende Versuch, und das frisch konstruierte TPDFDocument dahinter, aus dieser internen Kopie abspielt statt von wo auch immer ein gescheiterter Parse-Vorgang die Position des Streams hinterlassen hat. Übergeben Sie PDFlibPas einen TFileStream oder TMemoryStream für ein passwortgeschütztes Dokument, muss er zwischen erneuten Versuchen nicht zurückgespult werden; PDFlibPas berücksichtigt bereits eine Position, die ein erster, gescheiterter Versuch verschoben haben mag
Passwort-Retry in einen Dokumentenaufnahme-Bildschirm einpassen
Ein Dokumentenaufnahme-Workflow ist die natürliche Heimat für diesen Callback, weil es genau die Problemform ist, für die OnPassword gebaut wurde, zu lösen: Eine Datei kommt von außerhalb der Anwendung an, ihr Passwort ist im Voraus nicht mit Sicherheit bekannt, und die Person, die Kandidaten liefert, braucht mehr als einen Ratversuch, ohne dass der umgebende Code seine eigene Retry-Schleife um LoadFromFile herum schreibt
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 bekommt Lib nur, sobald LoadFromFile 1 zurückgegeben hat, was bedeutet, dass irgendein Passwort in diesem Austausch tatsächlich gegen den Crypt-Handler der Datei verifiziert hat; ein abgelehnter Versuch erreicht diese Zeile nie, und ein halb geöffnetes Dokument ebenfalls nicht. Was als Nächstes kommt, sobald ein solches Dokument als geöffnet bestätigt ist, lohnt sich ein zweiter Blick auf seine Schutzeinstellungen statt der Annahme, das funktionierende Passwort sei die ganze Sicherheitsgeschichte: das Auditieren dessen, was das /Encrypt-Wörterbuch eines Dokuments tatsächlich deklariert behandelt das Lesen des Algorithmus, der Revision und der Berechtigungs-Bits, die PDFlibPas offenlegt, sobald eine solche Datei lädt
Passwort-Retry ist auch eine enge Instanz einer breiteren Disziplin, die PDFlibPas durchgängig in seiner Parsing-Schicht anwendet: Eine Datei, die sich noch nicht bewährt hat, bekommt keinen Vertrauensvorschuss, egal ob die Frage lautet, welches Passwort sie entsperrt, oder ob ein Längenfeld darin über die Puffergröße lügt, die es braucht. Das Härten eines Pascal-PDF-Parsers gegen bösartige Dateien behandelt die andere Hälfte dieser Disziplin, die Decoder, die jedes Font-Programm und jeden Bild-Stream in einem eingehenden PDF als feindselige Eingabe behandeln statt als wohlgeformtes Dokument, das nur sein Passwort vergessen hat
OnPassword und die Retry-Schleife dahinter sind Teil der Standard-PDFlibPas-PDF-Bibliothek für Delphi und C++Builder, verfügbar überall, wo LoadFromFile, LoadFromStream oder LoadFromString bereits sind, ohne separates Modul oder Lizenzstufe für ein Dokument, das nur einen zweiten Ratversuch bei seinem Passwort braucht