PDFlibPas reîncearcă o parolă greșită pe un PDF criptat aruncând TPDFDocument-ul care tocmai a eșuat și creând unul complet nou pentru încercarea următoare, condus de un callback OnPassword (TPDFlibPasswordEvent) care rulează pentru până la șaisprezece încercări înainte de a renunța. Aceasta este o abatere deliberată de la instinctul la care recurg mai întâi majoritatea dezvoltatorilor Delphi: păstrați obiectul de document deja așezat în memorie, alimentați-l cu o parolă corectată, și încărcați din nou pe loc, în loc să reîncepeți de la nimic. Bucla de reîncercare a PDFlibPas, adăugată în v3.245.0, ia poziția opusă, din motive specifice a ceea ce lasă în urmă o încercare de parolă eșuată. Scenariul din spate este suficient de obișnuit încât majoritatea aplicațiilor Delphi intensive în documente îl întâlnesc în cele din urmă: un ecran de admisie acceptă un PDF, un trailer criptat forțează un dialog de parolă, operatorul greșește tastarea șirului, iar dialogul reapare pentru o a doua încercare. Nimic din acea experiență de utilizator nu este neobișnuit, așa că codul din spate trebuie să accepte mai mult de o parolă candidat pentru același fișier, și trebuie să facă asta în siguranță, fără a scurge stare din încercarea respinsă în cea care urmează
De ce nu puteți doar reîncerca pe același obiect de document?
Refolosirea unui TPDFDocument peste încercări de parolă nu funcționează, pentru că o încercare eșuată a demontat deja acel obiect intern, în loc să-l lase într-o stare de pauză, reluabilă. Deschiderea unui PDF criptat înseamnă analizarea tabelului de referință încrucișată, construirea unui cititor peste sursa de bază, și construirea unui handler de criptare din orice parolă a fost furnizată, toate acestea înainte ca PDFlibPas să poată măcar testa dacă acea parolă este corectă. Când parola se dovedește greșită, rutina internă de încărcare a documentului curăță cititorul, tabelul de referință încrucișată, și handler-ul de criptare ca parte a eșuării, exact așa cum ar trebui, ceea ce înseamnă că nu există niciun parser pe jumătate construit care stă acolo așteptând o parolă corectată la un al doilea apel. Conduceți totuși același obiect printr-o altă încercare de încărcare, iar modul de eșec este exact genul care este mizerabil de depanat: o eroare apare din starea internă construită pentru o analiză diferită, deja eșuată, cu nimic din ea care indică evident înapoi la parolă trei apeluri în amonte. PDFlibPas evită întreaga clasă de probleme nefiind niciodată tentat să recupereze un obiect de document odată ce a eșuat să se deschidă; fiecare încercare primește un document care nu a văzut niciodată o parolă greșită, cititor și tabel de referință încrucișată incluse
Cum cere callback-ul OnPassword următoarea parolă?
TPDFlibPasswordEvent este tipul de callback pe care PDFlibPas îl invocă prin TPDFlib.LoadFromFile, LoadFromStream și LoadFromString ori de câte ori parola tocmai încercată se dovedește greșită, și predă handler-ului trei lucruri: care încercare urmează să ruleze, un parametru Password de suprascris cu următorul candidat, și un steag Retry care este implicit false
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
Parola transmisă în apelul original LoadFromFile contează ca încercarea unu, așa că prima dată când OnPassword se declanșează deloc, AttemptNumber sosește ca 2. Lăsați Retry nesetat, iar încărcarea eșuează curat cu LastErrorCode 404; setați-l true, iar PDFlibPas încearcă din nou cu orice tocmai a scris handler-ul în Password
În interiorul buclei de reîncercare: un TPDFDocument nou pentru fiecare încercare
Intern, PDFlibPas răspunde la întrebarea de ciclu de viață al obiectului la fel pentru LoadFromFile, LoadFromStream și LoadFromString: fiecare încercare, incluzând prima, construiește un TPDFDocument proaspăt, îl rulează prin secvența completă de deschidere cu orice parolă folosește acea încercare, și păstrează obiectul doar dacă parola se verifică. TPDFDocument-ul unei încercări respinse este eliberat imediat, luând cu el cititorul, tabelul de referință încrucișată, și handler-ul de criptare, iar următoarea încercare reîncepe cu un obiect care nu are niciun istoric deloc
// 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;
Acea linie Doc := nil chiar înainte de blocul Finally este întregul contract de ciclu de viață al obiectului într-o singură instrucțiune. Un document care eșuează își poartă starea de parser pe jumătate construită în mormânt cu el, prin design, iar un document care reușește este singurul vreodată adăugat la FDocs, colecția pe care TPDFlib o păstrează pentru fiecare document pe care apelantul îl are deschis. Nimic despre o încercare respinsă nu este vizibil din afara buclei de reîncercare: niciun cititor pe jumătate inițializat, niciun număr de pagină perimat, niciun handler de criptare construit din cheia greșită
De câte ori va reîncerca PDFlibPas o parolă greșită?
PDFlibPas permite șaisprezece încercări totale față de un singur apel LoadFromFile, LoadFromStream, sau LoadFromString, numărând parola transmisă în apelul însuși ca încercarea unu. OnPassword se declanșează doar pentru încercările doi până la șaisprezece, ceea ce plafonează callback-ul la cincisprezece invocări; cereți o a șaptesprezecea încercare, iar PDFlibPas refuză fără măcar a invoca handler-ul. Lăsați Retry la implicitul său de false la orice punct, sau epuizați toate cele șaisprezece încercări fără o parolă corectă, iar LoadFromFile returnează 0 cu LastErrorCode setat la 404, codul PDFlibPas pentru o parolă respinsă. Plafonul există din motive dincolo de ordine: o buclă de reîncercare nelimitată este o modalitate ușoară de a transforma o parolă tastată greșit într-o negare de serviciu accidentală față de orice thread rulează încărcarea, mai ales odată ce un handler este conectat la ceva automatizat, precum o listă de parole văzute anterior, în loc de un om care dă clic printr-un dialog. PDFlibPas de asemenea respectă Abort apelat pe instanța TPDFlib din interiorul handler-ului, întrucât Sender sosește ca acel același obiect, util în spatele unui buton Anulare pe un dialog de parolă, și oprește bucla de reîncercare la următoarea verificare indiferent de ce a fost setat Retry. O încărcare care eșuează dintr-un motiv altul decât o parolă greșită, un tabel de referință încrucișată deteriorat de exemplu, nu intră niciodată deloc în bucla de reîncercare: PDFlibPas raportează LastErrorCode 401 și se oprește după prima încercare, pentru că niciun număr de ghiciri de parolă nu repară un fișier structural rupt
Funcționează bucla de reîncercare la fel pentru fișiere, fluxuri și șiruri?
Callback-ul OnPassword și plafonul de șaisprezece încercări se comportă identic pe LoadFromFile, LoadFromStream și LoadFromString, deși cele trei puncte de intrare țin de propria lor sursă diferit între încercări. O cale de fișier este ieftin de revizitat, întrucât fiecare încercare pur și simplu redeschide fișierul numit, iar o sursă de șir stă deja în memorie ca propria copie a apelantului, așa că niciuna nu are nevoie de niciun ajutor din partea apelantului între încercări. Un flux furnizat de apelant este singurul caz care merită o pauză: LoadFromStream derulează acel flux înapoi la poziția zero și îl copiază intern înainte de prima încercare de analiză, așa că fiecare încercare ulterioară, și TPDFDocument-ul proaspăt construit din spatele ei, redă din acea copie internă, în loc de oriunde a lăsat poziția fluxului o analiză eșuată. Predați PDFlibPas un TFileStream sau TMemoryStream pentru un document protejat prin parolă, iar nu este nevoie să îl derulați înapoi între reîncercări; PDFlibPas deja ține cont de o poziție pe care o primă încercare eșuată ar fi putut-o muta
Potrivirea reîncercării de parolă într-un ecran de admisie a documentelor
Un flux de lucru de admisie a documentelor este casa naturală pentru acest callback, pentru că este exact forma de problemă pentru care a fost construit OnPassword pentru a o rezolva: un fișier sosește din afara aplicației, parola sa nu este cunoscută cu certitudine în avans, iar persoana care furnizează candidați are nevoie de mai mult de o ghicire fără ca codul din jur să scrie propria buclă de reîncercare în jurul 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 primește Lib doar odată ce LoadFromFile a returnat 1, adică o anumită parolă din acel schimb chiar s-a verificat față de handler-ul de criptare al fișierului; o încercare respinsă nu ajunge niciodată la acea linie, și nici un document pe jumătate deschis. Ce urmează, odată ce un document ca acesta este confirmat deschis, merită o a doua privire asupra setărilor sale de protecție, în loc de o presupunere că parola care a funcționat este întreaga poveste de securitate: auditarea a ceea ce declară efectiv dicționarul /Encrypt al unui document acoperă citirea algoritmului, reviziei și biților de permisiune pe care PDFlibPas îi expune odată ce un fișier ca acesta se încarcă
Reîncercarea de parolă este de asemenea o instanță restrânsă a unei discipline mai largi pe care PDFlibPas o aplică pe tot parcursul stratului său de analiză: un fișier care nu s-a dovedit încă nu primește niciun beneficiu al îndoielii, fie că întrebarea este care parolă îl deblochează, fie dacă un câmp de lungime din interiorul lui minte despre dimensiunea de buffer de care are nevoie. Întărirea unui parser PDF Pascal împotriva fișierelor malițioase acoperă cealaltă jumătate a acelei discipline, decodoarele care tratează fiecare program de font și flux de imagine dintr-un PDF de intrare ca intrare adversarială, nu ca un document bine format care doar și-a uitat parola
OnPassword și bucla de reîncercare din spatele ei fac parte din biblioteca PDF PDFlibPas pentru Delphi și C++Builder standard, disponibilă oriunde LoadFromFile, LoadFromStream, sau LoadFromString sunt deja, fără niciun modul separat sau nivel de licență necesar pentru un document care doar are nevoie de o a doua ghicire a parolei sale