Articol tehnic

Reîncercarea parolelor PDF criptate în Delphi cu PDF Library for Delphi

PDF Library for Delphi 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 PDF Library for Delphi, 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 PDF Library for Delphi 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. PDF Library for Delphi 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 PDF Library for Delphi î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 PDF Library for Delphi î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, PDF Library for Delphi 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

Organigramă PDF Library for Delphi a buclei de reîncercare a parolei, în care fiecare încercare construiește un TPDFDocument nou, o încercare respinsă își eliberează cititorul, tabela de referințe încrucișate și handlerul de criptare, iar callback-ul OnPassword decide dacă rulează o altă încercare sau încărcarea eșuează cu LastErrorCode 404
Un candidat eșuat nu supraviețuiește niciodată buclei: documentul său este eliberat cu starea parser-ului încă pe jumătate construită. Setarea Retry scrie următorul candidat în Password și repetă cu un obiect care nu a văzut niciodată o parolă greșită
// Fragment simplificat din interiorul LoadFromFile: fiecare încercare primește un
// document care nu a văzut niciodată o parolă respinsă anterior. FileName,
// AttemptNumber și AttemptPassword provin din metoda înconjurătoare.
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);            // predă documentul verificat colecției
        Doc := nil;                 // apelantului; se sare peste Free de mai jos
      end;
    Finally
      Doc.Free;                     // cititorul, tabelul de referință încrucișată
    End;                            // și handler-ul de criptare ale unei încercări respinse sunt distruse chiar aici
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // succes, sau un eșec fără legătură cu parola: se oprește
    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 PDF Library for Delphi o parolă greșită?

PDF Library for Delphi 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 PDF Library for Delphi 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 PDF Library for Delphi 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. PDF Library for Delphi 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: PDF Library for Delphi 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

PDF Library for Delphi: pista încercărilor de la unu la șaisprezece, arătând OnPassword declanșat de la încercarea a doua înainte, încercarea șaptesprezece refuzată fără a invoca handlerul, și rezultatele separate pentru succes, LastErrorCode 404 și LastErrorCode 401
Doar parola transmisă chiar apelului de încărcare contează ca încercarea unu, iar callback-ul nu rulează niciodată mai mult de cincisprezece ori per încărcare. Un fișier structural defect ocolește complet bucla și raportează 401 după o singură încercare

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 PDF Library for Delphi un TFileStream sau TMemoryStream pentru un document protejat prin parolă, iar nu este nevoie să îl derulați înapoi între reîncercări; PDF Library for Delphi 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 numără de la 2: parola deja încercată a fost încercarea 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry este False când operatorul anulează, ceea ce lasă
  // LastErrorCode la 404 pentru ca apelantul să raporteze.
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)        // doar un document verificat ajunge aici
    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 PDF Library for Delphi î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 PDF Library for Delphi 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 PDF Library for Delphi 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