Tehnički članak

Ponovni pokušaj lozinke šifrovanog PDF-a u Delphi-ju uz PDF Library for Delphi

PDF Library for Delphi ponavlja pokušaj sa pogrešnom lozinkom na šifrovanom PDF-u tako što odbacuje TPDFDocument koji je upravo pretrpeo neuspeh i za sledeći pokušaj kreira potpuno novi, uz OnPassword povratni poziv (TPDFlibPasswordEvent) koji se izvršava do šesnaest pokušaja pre odustajanja. To je namerno odstupanje od prvog pristupa za kojim većina Delphi programera poseže: zadržati objekat dokumenta koji je već u memoriji, proslediti mu ispravljenu lozinku i ponovo ga učitati na istom mestu umesto da se sve započne iz početka. PDF Library for Delphi-ova petlja ponovnih pokušaja, dodata u v3.245.0, zauzima suprotan stav zbog onoga što neuspešan pokušaj lozinke ostavlja iza sebe. Scenario je dovoljno uobičajen da ga većina Delphi aplikacija koje intenzivno rade sa dokumentima pre ili kasnije doživi: ekran za prijem prihvati PDF, šifrovani trailer zahteva dijalog za lozinku, operater pogrešno unese tekst i dijalog se ponovo pojavi za drugi pokušaj. Ništa u tom korisničkom iskustvu nije neobično, pa kod mora da prihvati više kandidata lozinke za isti fajl i da to uradi bezbedno, bez prenošenja stanja iz odbijenog pokušaja u onaj koji sledi

Zašto se ne može ponovo pokušati sa istim objektom dokumenta

Ponovna upotreba TPDFDocument-a kroz više pokušaja lozinke ne funkcioniše, jer je neuspešan pokušaj već interno razgradio taj objekat umesto da ga ostavi u nekom pauziranom stanju iz kog se može nastaviti. Otvaranje šifrovanog PDF-a podrazumeva parsiranje unakrsne referentne tabele, izgradnju čitača nad osnovnim izvorom i kreiranje rukovaoca za šifrovanje na osnovu prosleđene lozinke, sve pre nego što PDF Library for Delphi uopšte može da proveri da li je lozinka ispravna. Kada se ispostavi da je lozinka pogrešna, interna rutina za učitavanje dokumenta pri prekidu čisti čitač, unakrsnu referentnu tabelu i rukovaoca za šifrovanje, upravo kako treba, što znači da tamo ne sedi poluizgrađen parser koji čeka ispravljenu lozinku pri drugom pozivu. Ako taj isti objekat ipak provučete kroz novi pokušaj učitavanja, dobićete upravo onu vrstu greške koju je mučno otklanjati: greška potiče iz internog stanja izgrađenog za drugačije, već neuspešno parsiranje, a ništa u njoj očigledno ne upućuje na lozinku od pre tri poziva. PDF Library for Delphi izbegava čitavu klasu problema tako što nikada ne pokušava da oporavi objekat dokumenta nakon neuspešnog otvaranja; svaki pokušaj dobija dokument koji nikada nije video pogrešnu lozinku, uključujući čitač i unakrsnu referentnu tabelu

Kako OnPassword povratni poziv traži sledeću lozinku

TPDFlibPasswordEvent je tip povratnog poziva koji PDF Library for Delphi poziva kroz TPDFlib.LoadFromFile, LoadFromStream i LoadFromString kada se upravo isprobana lozinka pokaže kao pogrešna, a rukovaocu prosleđuje tri stvari: koji pokušaj treba da se izvrši, parametar Password koji treba prepisati sledećim kandidatom i zastavicu Retry koja podrazumevano ima vrednost false

TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean) of object;

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Lozinka prosleđena prvobitnom pozivu LoadFromFile računa se kao prvi pokušaj, pa kada se OnPassword prvi put uopšte aktivira, AttemptNumber ima vrednost 2. Ako Retry ostavite nepromenjenim, učitavanje se uredno završava neuspehom uz LastErrorCode 404; ako ga postavite na true, PDF Library for Delphi ponovo pokušava sa onim što je rukovalac upravo upisao u Password

Unutar petlje pokušaja: novi TPDFDocument za svaki pokušaj

Interno, PDF Library for Delphi na isti način rešava pitanje životnog ciklusa objekta za LoadFromFile, LoadFromStream i LoadFromString: svaki pokušaj, uključujući prvi, kreira novi TPDFDocument, provodi ga kroz potpun niz otvaranja sa lozinkom koju taj pokušaj koristi i zadržava objekat samo ako provera lozinke uspe. TPDFDocument odbijenog pokušaja odmah se oslobađa, zajedno sa svojim čitačem, unakrsnom referentnom tabelom i rukovaocem za šifrovanje, a sledeći pokušaj počinje iznova sa objektom koji nema nikakvu istoriju

PDF Library for Delphi dijagram toka petlje ponavljanja lozinke gde svaki pokušaj gradi svež TPDFDocument, odbijeni pokušaj oslobađa njegov čitač, tabelu unakrsnih referenci i crypt rukovaoca, a OnPassword callback odlučuje da li se pokreće još jedan pokušaj ili učitavanje propada sa LastErrorCode 404
Neuspešan kandidat nikada ne preživljava petlju: njegov dokument se oslobađa sa stanjem parsera još uvek napola izgrađenim. Postavljanje Retry upisuje sledećeg kandidata u Password i ponavlja sa objektom koji nikada nije video pogrešnu lozinku
// Pojednostavljen isečak iz unutrašnjosti LoadFromFile: svaki pokušaj dobija
// dokument koji nikada nije video prethodno odbijenu lozinku. FileName,
// AttemptNumber i AttemptPassword dolaze iz metode koja ga obuhvata.
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);            // predaje potvrđeni dokument
        Doc := nil;                 // kolekciji pozivaoca; preskoči Free ispod
      end;
    Finally
      Doc.Free;                     // čitač, unakrsna referentna tabela odbijenog pokušaja
    End;                            // i rukovalac za šifrovanje se ovde uništavaju
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // uspeh, ili neuspeh koji nije zbog lozinke: zaustavi
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Linija Doc := nil neposredno pre bloka Finally predstavlja ceo ugovor životnog ciklusa objekta u jednoj naredbi. Dokument koji ne uspe namerno sa sobom odnosi u uništenje svoje poluizgrađeno stanje parsera, a dokument koji uspe jedini se ikada dodaje u FDocs, kolekciju koju TPDFlib održava za svaki dokument koji je pozivalac otvorio. Ništa iz odbijenog pokušaja nije vidljivo izvan petlje ponovnih pokušaja: ni poluinicijalizovani čitač, ni zastareo broj stranica, ni rukovalac za šifrovanje izgrađen iz pogrešnog ključa

Koliko puta će PDF Library for Delphi ponoviti pogrešnu lozinku

PDF Library for Delphi dozvoljava ukupno šesnaest pokušaja u okviru jednog poziva LoadFromFile, LoadFromStream ili LoadFromString, pri čemu se lozinka prosleđena samom pozivu računa kao prvi pokušaj. OnPassword se aktivira samo za pokušaje od dva do šesnaest, pa je broj poziva povratnog poziva ograničen na petnaest; ako zatražite sedamnaesti pokušaj, PDF Library for Delphi ga odbija čak i bez pozivanja rukovaoca. Ako u bilo kom trenutku ostavite Retry na podrazumevanom false ili potrošite svih šesnaest pokušaja bez ispravne lozinke, LoadFromFile vraća 0 uz LastErrorCode 404, šifru PDF Library for Delphi-a za odbijenu lozinku. Ovo ograničenje postoji iz važnijih razloga od urednosti: neograničena petlja ponovnih pokušaja lako može jednu pogrešno unetu lozinku pretvoriti u nenamerno uskraćivanje usluge niti koja izvršava učitavanje, naročito kada je rukovalac povezan sa automatizovanim izvorom, poput spiska ranije viđenih lozinki, umesto sa čovekom koji klikće kroz dijalog. PDF Library for Delphi takođe poštuje Abort pozvan na instanci TPDFlib-a iz rukovaoca, pošto Sender stiže kao taj isti objekat, što je korisno iza dugmeta Cancel u dijalogu za lozinku, i zaustavlja petlju pri sledećoj proveri bez obzira na vrednost Retry. Učitavanje koje ne uspe iz razloga koji nije pogrešna lozinka, na primer zbog oštećene unakrsne referentne tabele, uopšte ne ulazi u petlju ponovnih pokušaja: PDF Library for Delphi prijavljuje LastErrorCode 401 i zaustavlja se posle prvog pokušaja, jer nijedan broj pogađanja lozinke ne može popraviti strukturno oštećen fajl

PDF Library for Delphi: Trasa pokušaja od jedan do šesnaest koja pokazuje da se OnPassword okida od drugog pokušaja nadalje, da je sedamnaesti pokušaj odbijen bez pozivanja rukovaoca, i odvojene ishode za uspeh, LastErrorCode 404 i LastErrorCode 401
Samo lozinka data samom pozivu učitavanja računa se kao prvi pokušaj, i povratni poziv nikada ne radi više od petnaest puta po učitavanju. Strukturno oštećena datoteka zaobilazi petlju u celosti i javlja 401 posle jednog pokušaja

Da li petlja pokušaja isto radi za datoteke, tokove i stringove

OnPassword povratni poziv i ograničenje od šesnaest pokušaja ponašaju se isto kroz LoadFromFile, LoadFromStream i LoadFromString, iako tri ulazne tačke različito zadržavaju svoj izvor između pokušaja. Putanju do fajla lako je ponovo koristiti, jer svaki pokušaj jednostavno ponovo otvara imenovani fajl, a string izvor je već u memoriji kao sopstvena kopija pozivaoca, pa nijednom nije potrebna pomoć pozivaoca između pokušaja. Tok koji prosleđuje pozivalac jedini je slučaj vredan posebne pažnje: LoadFromStream vraća tok na poziciju nula i interno ga kopira pre prvog pokušaja parsiranja, pa svaki sledeći pokušaj, kao i sveže kreirani TPDFDocument iza njega, ponavlja čitanje iz te interne kopije umesto iz mesta na koje je neuspešno parsiranje pomerilo poziciju toka. Kada PDF Library for Delphi-u prosledite TFileStream ili TMemoryStream za dokument zaštićen lozinkom, nema potrebe da ga vraćate unazad između pokušaja; PDF Library for Delphi već uzima u obzir poziciju koju je prvi, neuspešni pokušaj možda promenio

Uklapanje ponovnog pokušaja lozinke u ekran za prijem dokumenata

Tok rada za prijem dokumenata prirodno je mesto za ovaj povratni poziv, jer upravo rešava problem za koji je OnPassword napravljen: fajl stiže izvan aplikacije, njegova lozinka unapred nije pouzdano poznata, a osoba koja unosi kandidate mora da ima više od jednog pokušaja bez pisanja sopstvene petlje ponovnih pokušaja oko LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber broji od 2: lozinka koja je već isprobana bila je pokušaj 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry je False kada operater otkaže, što ostavlja
  // LastErrorCode na 404 da ga pozivalac prijavi.
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)        // samo potvrđen dokument stiže ovde
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument prima Lib tek kada LoadFromFile vrati 1, što znači da je neka lozinka iz te razmene zaista potvrđena u odnosu na rukovaoca za šifrovanje fajla; odbijeni pokušaj nikada ne stiže do te linije, kao ni poluotvoren dokument. Kada se potvrdi da je ovakav dokument otvoren, vredi još jednom pogledati njegova podešavanja zaštite umesto pretpostaviti da lozinka koja je radila predstavlja celu bezbednosnu priču: provera onoga što rečnik /Encrypt dokumenta zaista objavljuje obuhvata čitanje algoritma, revizije i bitova dozvola koje PDF Library for Delphi izlaže kada se ovakav fajl učita

Ponovni pokušaj lozinke je takođe uska primena šire discipline koju PDF Library for Delphi primenjuje kroz ceo sloj za parsiranje: fajl koji još nije dokazao da je ispravan ne dobija nikakvo poverenje unapred, bilo da je pitanje koja lozinka ga otključava ili da li polje dužine u njemu lažno navodi veličinu bafera koja mu je potrebna. Ojačavanje Pascal PDF parsera protiv zlonamernih fajlova obrađuje drugu polovinu te discipline, odnosno dekodere koji svaki program fonta i tok slike u pristiglom PDF-u tretiraju kao neprijateljski ulaz, a ne kao ispravan dokument koji je samo zaboravio lozinku

OnPassword i petlja ponovnih pokušaja iza njega deo su standardne PDF Library for Delphi PDF biblioteke za Delphi i C++Builder, dostupne svuda gde su već dostupni LoadFromFile, LoadFromStream ili LoadFromString, bez potrebe za posebnim modulom ili licencnim nivoom kada dokumentu treba samo drugi pokušaj lozinke