PDFlibPas pakartotinai bando neteisingą slaptažodį užšifruotame PDF faile: atsisako ką tik nepavykusio TPDFDocument ir kitam bandymui sukuria visiškai naują objektą, naudodamas OnPassword atgalinį iškvietimą (TPDFlibPasswordEvent), kuris vykdomas iki šešiolikos bandymų prieš nutraukiant veiksmą. Tai sąmoningas nukrypimas nuo pirmo sprendimo, kurio dažniausiai griebiasi Delphi kūrėjai: palikti atmintyje esantį dokumento objektą, perduoti jam pataisytą slaptažodį ir įkelti dokumentą iš naujo vietoje, o ne pradėti nuo nulio. PDFlibPas pakartotinių bandymų ciklas, pridėtas v3.245.0, renkasi priešingą kryptį dėl konkrečių priežasčių, susijusių su tuo, kas lieka po nesėkmingo slaptažodžio bandymo. Šis scenarijus toks įprastas, kad anksčiau ar vėliau su juo susiduria dauguma dokumentais dirbančių Delphi programų: priėmimo forma gauna PDF, užšifruota priekaba parodo slaptažodžio dialogą, operatorius netyčia įveda neteisingą eilutę, o dialogas pasirodo dar kartą. Tokia naudotojo patirtis nėra neįprasta, todėl kodas turi saugiai priimti daugiau nei vieną galimą to paties failo slaptažodį ir neleisti, kad atmesto bandymo būsena nutekėtų į kitą bandymą
Kodėl negalima tiesiog bandyti dar kartą su tuo pačiu dokumento objektu?
Pakartotinis TPDFDocument naudojimas tarp slaptažodžio bandymų neveikia, nes po nesėkmingo bandymo objektas viduje jau būna išardytas, o ne paliktas pristabdytas ir paruoštas tęsti. Atveriant užšifruotą PDF reikia išanalizuoti kryžminių nuorodų lentelę, sukurti skaitytuvą virš pagrindinio šaltinio ir suformuoti šifravimo apdorojimo objektą pagal pateiktą slaptažodį, ir visa tai įvyksta dar prieš PDFlibPas patikrinant, ar slaptažodis teisingas. Paaiškėjus, kad slaptažodis neteisingas, vidinė dokumento įkėlimo procedūra, kaip ir turi būti, nesėkmingo veiksmo metu sutvarko skaitytuvą, kryžminių nuorodų lentelę ir šifravimo apdorojimo objektą, todėl nelieka pusiau sukurto analizatoriaus, laukiančio pataisyto slaptažodžio per antrą iškvietimą. Vis dėlto bandant dar kartą naudoti tą patį objektą, gedimas tampa ypač sunkiai diagnozuojamas: klaida kyla iš vidinės būsenos, sukurtos kitam jau nepavykusiam analizavimui, ir niekas aiškiai nenurodo į slaptažodį, pateiktą trimis iškvietimais anksčiau. PDFlibPas išvengia visos šios problemų klasės niekada nemėgindamas atkurti dokumento objekto, kuriam nepavyko atverti failo: kiekvienam bandymui sukuriamas dokumentas, dar nematęs neteisingo slaptažodžio, kartu su skaitytuvu ir kryžminių nuorodų lentele
Kaip OnPassword atgalinis iškvietimas paprašo kito slaptažodžio?
TPDFlibPasswordEvent yra atgalinio iškvietimo tipas, kurį PDFlibPas iškviečia per TPDFlib.LoadFromFile, LoadFromStream ir LoadFromString, kai ką tik išbandytas slaptažodis pasirodo esąs neteisingas, ir tvarkyklei perduoda tris dalykus: bandymo numerį, kuris netrukus bus vykdomas, parametrą Password, kurį reikia perrašyti kitu kandidatu, ir Retry vėliavėlę, kurios numatytoji reikšmė yra false
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
Slaptažodis, perduotas pradiniam LoadFromFile iškvietimui, laikomas pirmuoju bandymu, todėl pirmą kartą suveikus OnPassword parametrą AttemptNumber gausite lygų 2. Jei Retry paliksite nenustatytą, įkėlimas tvarkingai baigsis su LastErrorCode 404; jei nustatysite true, PDFlibPas bandys dar kartą su tuo, ką tvarkyklė ką tik įrašė į Password
Pakartotinių bandymų ciklo vidus: naujas TPDFDocument kiekvienam bandymui
Viduje PDFlibPas vienodai atsako į objekto gyvavimo ciklo klausimą, nesvarbu, ar naudojamas LoadFromFile, LoadFromStream, ar LoadFromString: kiekvienam bandymui, įskaitant pirmąjį, sukuriamas naujas TPDFDocument, jam atliekama visa atvėrimo seka su to bandymo slaptažodžiu ir objektas pasiliekamas tik tada, kai slaptažodis patikrinamas. Atmesto bandymo TPDFDocument iškart atlaisvinamas kartu su skaitytuvu, kryžminių nuorodų lentele ir šifravimo apdorojimo objektu, o kitas bandymas pradedamas su objektu, neturinčiu jokios istorijos
// 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;
Ši Doc := nil eilutė prieš pat Finally visą objekto gyvavimo ciklo sutartį išreiškia vienu sakiniu. Nepavykęs dokumentas tyčia nusineša pusiau sukurtą analizatoriaus būseną, o sėkmingas dokumentas yra vienintelis, įtraukiamas į FDocs – rinkinį, kurį TPDFlib saugo kiekvienam atvertam dokumentui. Iš atmesto bandymo už pakartotinių bandymų ciklo ribų nieko nematyti: nei pusiau inicializuoto skaitytuvo, nei pasenusio puslapių skaičiaus, nei iš neteisingo rakto sukurto šifravimo apdorojimo objekto
Kiek kartų PDFlibPas bandys neteisingą slaptažodį?
PDFlibPas vienam LoadFromFile, LoadFromStream arba LoadFromString iškvietimui leidžia iš viso šešiolika bandymų, pradiniam iškvietimui perduotą slaptažodį laikydamas pirmuoju. OnPassword suveikia tik bandymams nuo antrojo iki šešioliktojo, todėl atgalinis iškvietimas daugiausia iškviečiamas penkiolika kartų; paprašius septyniolikto bandymo, PDFlibPas atsisako jo net neiškvietęs tvarkyklės. Jei bet kuriuo momentu Retry paliekama numatytoji false reikšmė arba visi šešiolika bandymų baigiami be teisingo slaptažodžio, LoadFromFile grąžina 0, o LastErrorCode nustatomas į 404 – PDFlibPas kodą atmestam slaptažodžiui. Ši riba reikalinga ne vien tvarkai palaikyti: neribotas pakartotinių bandymų ciklas gali lengvai paversti vieną rašybos klaidą slaptažodyje netyčine paslaugos atsisakymo ataka prieš giją, vykdančią įkėlimą, ypač kai tvarkyklė prijungta prie automatizuoto šaltinio, pavyzdžiui, anksčiau matytų slaptažodžių sąrašo, o ne prie žmogaus, spaudinėjančio dialogo mygtukus. PDFlibPas taip pat paiso iš tvarkyklės iškviesto TPDFlib egzemplioriaus Abort, nes Sender perduodamas kaip tas pats objektas; tai naudinga slaptažodžio dialogo Atšaukti mygtukui ir sustabdo pakartotinių bandymų ciklą per kitą patikrą, nepaisant to, kokia Retry reikšmė buvo nustatyta. Jei įkėlimas nepavyksta dėl kitos priežasties nei neteisingas slaptažodis, pavyzdžiui, dėl sugadintos kryžminių nuorodų lentelės, į pakartotinių bandymų ciklą apskritai nepatenkama: PDFlibPas praneša LastErrorCode 401 ir sustoja po pirmojo bandymo, nes joks slaptažodžių spėliojimas nepataisys struktūriškai sugadinto failo
Ar pakartotinių bandymų ciklas vienodai veikia failams, srautams ir eilutėms?
OnPassword atgalinis iškvietimas ir šešiolikos bandymų riba vienodai veikia su LoadFromFile, LoadFromStream ir LoadFromString, tačiau šie trys įvesties taškai skirtingai išlaiko savo šaltinį tarp bandymų. Failo kelią aplankyti dar kartą paprasta, nes kiekvienas bandymas tik iš naujo atveria nurodytą failą, o eilutės šaltinis jau yra atmintyje kaip paties kviečiančiojo kopija, todėl tarp bandymų nė vienam nereikia kviečiančiojo pagalbos. Vienintelis atvejis, į kurį verta atkreipti dėmesį, yra kviečiančiojo pateiktas srautas: LoadFromStream prieš pirmą analizavimo bandymą grąžina srautą į nulio poziciją ir nukopijuoja jį viduje, todėl kiekvienas tolesnis bandymas ir už jo sukurtas naujas TPDFDocument kartojamas iš vidinės kopijos, o ne iš vietos, į kurią srauto poziciją galėjo perkelti nepavykęs analizavimas. Perduodant TFileStream arba TMemoryStream slaptažodžiu apsaugotam dokumentui, tarp pakartotinių bandymų jo pervynioti nereikia; PDFlibPas jau atsižvelgia į poziciją, kurią galėjo pakeisti pirmasis nesėkmingas bandymas
Slaptažodžio pakartotinio bandymo pritaikymas dokumentų priėmimo lange
Dokumento priėmimo eiga yra natūrali vieta šiam atgaliniam iškvietimui, nes būtent tokiai problemai OnPassword ir sukurtas: failas atkeliauja iš išorinės sistemos, jo slaptažodis iš anksto nėra patikimai žinomas, o kandidatus pateikiančiam žmogui reikia daugiau nei vieno bandymo, nekuriant atskiro pakartotinių bandymų ciklo aplink 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 gauna Lib tik tada, kai LoadFromFile grąžina 1, vadinasi, tam tikras šio apsikeitimo slaptažodis iš tikrųjų buvo patikrintas pagal failo šifravimo apdorojimo objektą; atmestas bandymas šios eilutės nepasiekia, kaip ir pusiau atvertas dokumentas. Kai toks dokumentas patvirtinamas kaip atvertas, verta dar kartą pažvelgti į jo apsaugos nustatymus, o ne manyti, kad suveikęs slaptažodis yra visa saugumo istorija: dokumento /Encrypt žodyne deklaruojamų dalykų auditas apima algoritmo, revizijos ir leidimų bitų, kuriuos PDFlibPas atskleidžia įkėlus tokį failą, nuskaitymą
Slaptažodžio pakartotinis bandymas taip pat yra siauras platesnės PDFlibPas taikomos analizavimo disciplinos atvejis: failas, kuris dar neįrodė savo tinkamumo, nesulaukia jokios lengvatos, nesvarbu, ar sprendžiama, kuris slaptažodis jį atrakins, ar ilgio laukas meluoja apie jam reikalingo buferio dydį. Pascal PDF analizatoriaus apsauga nuo kenkėjiškų failų aprašo kitą šios disciplinos pusę – dekoderius, kurie kiekvieną įeinančio PDF šrifto programą ir vaizdo srautą vertina kaip priešišką įvestį, o ne kaip tvarkingą dokumentą, kuris tik pamiršo savo slaptažodį
OnPassword ir už jo esantis pakartotinių bandymų ciklas yra standartinės PDFlibPas PDF bibliotekos, skirtos Delphi ir C++Builder, dalis, pasiekiama visur, kur jau pasiekiami LoadFromFile, LoadFromStream arba LoadFromString, todėl dokumentui, kuriam reikia tik dar vieno slaptažodžio bandymo, nereikia atskiro modulio ar licencijos lygio