PDFlibPas 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. PDFlibPas-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 PDFlibPas 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. PDFlibPas 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 PDFlibPas 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, PDFlibPas ponovo pokušava sa onim što je rukovalac upravo upisao u Password
Unutar petlje pokušaja: novi TPDFDocument za svaki pokušaj
Interno, PDFlibPas 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
// 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;
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 PDFlibPas ponoviti pogrešnu lozinku
PDFlibPas 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, PDFlibPas 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 PDFlibPas-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. PDFlibPas 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: PDFlibPas 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
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 PDFlibPas-u prosledite TFileStream ili TMemoryStream za dokument zaštićen lozinkom, nema potrebe da ga vraćate unazad između pokušaja; PDFlibPas 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 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 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 PDFlibPas izlaže kada se ovakav fajl učita
Ponovni pokušaj lozinke je takođe uska primena šire discipline koju PDFlibPas 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 PDFlibPas 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