Tehnički članak

Šifrirani PDF u Delphi-ju: životni ciklus ponovnog pokušaja lozinke

PDF Library for Delphi ponovno pokušava pogrešnu lozinku za šifrirani PDF tako da odbacuje TPDFDocument koji je upravo zakazao i stvara potpuno novi za sljedeći pokušaj, uz OnPassword povratni poziv (TPDFlibPasswordEvent) koji se izvršava najviše šesnaest puta prije odustajanja. To je namjerno odstupanje od prvog pristupa za kojim većina Delphi programera posegne: zadržati objekt dokumenta koji već postoji u memoriji, proslijediti mu ispravljenu lozinku i ponovno učitati dokument na mjestu umjesto da se krene ispočetka. PDFlibPasova petlja za ponovni pokušaj, dodana u v3.245.0, zauzima suprotan stav zbog onoga što neuspjeli pokušaj lozinke ostavlja iza sebe. Scenarij je dovoljno uobičajen da ga većina Delphi aplikacija koje intenzivno rade s dokumentima prije ili poslije doživi: zaslon za prihvat učita PDF, šifrirani trailer zatraži dijalog za lozinku, operater pogrešno upiše niz, a dijalog se ponovno pojavi za drugi pokušaj. Ništa u tom korisničkom iskustvu nije neobično, pa kod mora prihvatiti više kandidata lozinke za istu datoteku i to učiniti sigurno, bez propuštanja stanja odbijenog pokušaja u onaj koji slijedi

Zašto jednostavno ne možete ponovno pokušati s istim objektom dokumenta?

Ponovna uporaba objekta TPDFDocument kroz više pokušaja lozinke ne funkcionira, jer neuspjeli pokušaj već interno rastavi taj objekt umjesto da ga ostavi u nekom zaustavljenom stanju iz kojeg se može nastaviti. Otvaranje šifriranog PDF-a znači raščlaniti unakrsnu referentnu tablicu, izgraditi čitač nad temeljnim izvorom i konstruirati rukovatelj kriptografijom iz dostavljene lozinke, sve prije nego što PDF Library for Delphi uopće može provjeriti je li lozinka točna. Kada se pokaže da lozinka nije točna, interna rutina učitavanja dokumenta tijekom prekida očisti čitač, unakrsnu referentnu tablicu i rukovatelj kriptografijom, upravo kako treba, što znači da ondje nema napola izgrađenog parsera koji bi čekao ispravljenu lozinku pri drugom pozivu. Ako taj isti objekt ipak provedete kroz novi pokušaj učitavanja, dobit ćete upravo onu vrstu greške koju je teško otkloniti: greška se pojavi iz internog stanja izgrađenog za drugi, već neuspjeli pokušaj raščlambe, bez ičega što bi je očito povezalo s lozinkom unesenom tri poziva ranije. PDF Library for Delphi izbjegava cijelu tu skupinu problema tako što nikada ne pokušava oporaviti objekt dokumenta nakon neuspjelog otvaranja; svaki pokušaj dobiva dokument koji još nije vidio pogrešnu lozinku, uključujući čitač i unakrsnu referentnu tablicu

Kako OnPassword povratnim pozivom traži sljedeću lozinku?

TPDFlibPasswordEvent je vrsta povratnog poziva koju PDF Library for Delphi poziva kroz TPDFlib.LoadFromFile, LoadFromStream i LoadFromString kad se pokaže da je upravo isprobana lozinka pogrešna, a rukovatelju predaje tri stvari: koji će se pokušaj pokrenuti, parametar Password koji treba prebrisati sljedećim kandidatom i zastavicu Retry koja je prema zadanim postavkama false

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Lozinka predana izvornom pozivu LoadFromFile računa se kao prvi pokušaj, pa prvi put kada se OnPassword uopće aktivira, AttemptNumber ima vrijednost 2. Ako Retry ostavite nepostavljenim, učitavanje će se uredno neuspješno završiti s LastErrorCode 404; postavite ga na true i PDF Library for Delphi će ponovno pokušati s onim što je rukovatelj upravo upisao u Password

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

Interno PDF Library for Delphi na pitanje životnog ciklusa objekta odgovara na isti način za LoadFromFile, LoadFromStream i LoadFromString: svaki pokušaj, uključujući prvi, konstruira novi TPDFDocument, provodi ga kroz cijeli slijed otvaranja s lozinkom koju taj pokušaj koristi i zadržava objekt samo ako se lozinka potvrdi. TPDFDocument odbijenog pokušaja odmah se oslobađa, a s njim nestaju i čitač, unakrsna referentna tablica te rukovatelj kriptografijom, dok sljedeći pokušaj počinje s objektom koji nema nikakvu povijest

PDF Library for Delphi dijagram toka petlje ponovnih pokušaja lozinke u kojem svaki pokušaj gradi novi TPDFDocument, odbijeni pokušaj oslobađa njegov čitač, tablicu unakrsnih referenci i rukovatelj šifriranjem, a OnPassword povratni poziv odlučuje hoće li se pokrenuti još jedan pokušaj ili učitavanje propada s LastErrorCode 404
Neuspjeli kandidat nikad ne preživi petlju: njegov se dokument oslobađa dok je stanje parsera još napola izgrađeno. Postavljanje Retry sljedećeg kandidata upisuje u Password i ponavlja s objektom koji nikad nije vidio krivu lozinku
// Pojednostavljeni izvadak iz LoadFromFile: svaki pokušaj dobiva
// dokument koji nikada nije vidio prethodno odbijenu lozinku. FileName,
// AttemptNumber i AttemptPassword dolaze iz metode koja ovo poziva.
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);            // proslijedi provjereni dokument
        Doc := nil;                 // kolekciji pozivatelja; preskoči Free ispod
      end;
    Finally
      Doc.Free;                     // čitač, xref tablica i handler za enkripciju
    End;                            // odbijenog pokušaja ruše se upravo ovdje
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // uspjeh, ili greška koja nije zbog lozinke: zaustavi
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Redak Doc := nil neposredno prije bloka Finally cijeli je ugovor životnog ciklusa objekta izražen jednom naredbom. Dokument koji ne uspije nestaje zajedno sa svojim napola izgrađenim stanjem parsera, namjerno, a dokument koji uspije jedini se dodaje u FDocs, zbirku koju TPDFlib održava za svaki dokument koji je pozivatelj otvorio. Izvan petlje ponovnog pokušaja ništa od odbijenog pokušaja nije vidljivo: ni napola inicijalizirani čitač, ni zastarjeli broj stranica, ni rukovatelj kriptografijom izgrađen iz pogrešnog ključa

Koliko će puta PDF Library for Delphi ponovno pokušati s pogrešnom lozinkom?

PDF Library for Delphi dopušta ukupno šesnaest pokušaja za jedan poziv LoadFromFile, LoadFromStream ili LoadFromString, pri čemu se lozinka predana samom pozivu računa kao prvi pokušaj. OnPassword se ikada aktivira samo za pokušaje od drugog do šesnaestog, pa je broj poziva povratnog poziva ograničen na petnaest; zatražite sedamnaesti pokušaj i PDF Library for Delphi će ga odbiti bez ikakvog poziva rukovatelju. Ako u bilo kojem trenutku ostavite Retry na zadanoj vrijednosti false ili iscrpite svih šesnaest pokušaja bez točne lozinke, LoadFromFile vraća 0, a LastErrorCode postavlja na 404, što je PDFlibPasov kod za odbijenu lozinku. Ograničenje postoji iz razloga koji nadilaze urednost: neograničena petlja ponovnih pokušaja jednostavan je način da se jedna pogrešno upisana lozinka pretvori u slučajni napad uskraćivanja usluge nad niti koja obavlja učitavanje, osobito kada je rukovatelj povezan s automatiziranim izvorom, poput popisa ranije viđenih lozinki, umjesto s čovjekom koji klikće kroz dijalog. PDF Library for Delphi također uvažava poziv Abort na instanci TPDFlib iz samog rukovatelja, jer Sender stiže kao taj isti objekt, što je korisno iza gumba Odustani u dijalogu za lozinku, te zaustavlja petlju ponovnog pokušaja pri sljedećoj provjeri bez obzira na to kako je Retry postavljen. Učitavanje koje ne uspije zbog razloga koji nije pogrešna lozinka, primjerice zbog oštećene unakrsne referentne tablice, uopće ne ulazi u petlju ponovnog pokušaja: PDF Library for Delphi prijavljuje LastErrorCode 401 i zaustavlja se nakon prvog pokušaja, jer nijedan broj pogađanja lozinke ne može popraviti strukturno oštećenu datoteku

PDF Library for Delphi: staza pokušaja od jedan do šesnaest koja prikazuje OnPassword aktiviran od pokušaja dva nadalje, pokušaj sedamnaest odbijen bez pozivanja rukovatelja te odvojene ishode za uspjeh, LastErrorCode 404 i LastErrorCode 401
Samo lozinka predana samom pozivu učitavanja računa se kao prvi pokušaj, a povratni poziv nikad se ne izvršava više od petnaest puta po učitavanju. Strukturno oštećena datoteka cijelu petlju zaobilazi i nakon jednog pokušaja javlja 401

Radi li petlja ponovnog pokušaja jednako za datoteke, tokove i nizove?

Povratni poziv OnPassword i ograničenje od šesnaest pokušaja ponašaju se jednako za LoadFromFile, LoadFromStream i LoadFromString, iako te tri ulazne točke različito zadržavaju svoj izvor između pokušaja. Putanju datoteke je lako ponovno otvoriti jer svaki pokušaj jednostavno ponovno otvara imenovanu datoteku, a izvor u obliku niza već je u memoriji kao vlastita kopija pozivatelja, pa nijedan od njih ne traži pomoć pozivatelja između pokušaja. Tok koji predaje pozivatelj jedini je slučaj kod kojega vrijedi zastati: LoadFromStream vraća taj tok na poziciju nula i interno ga kopira prije prvog pokušaja raščlambe, pa se svaki sljedeći pokušaj i svježe konstruirani TPDFDocument iza njega ponovno pokreću iz te interne kopije, a ne s mjesta na kojem je neuspjela raščlamba ostavila poziciju toka. Predate li PDF Library for Delphi objekt TFileStream ili TMemoryStream za dokument zaštićen lozinkom, nema potrebe vraćati ga na početak između pokušaja; PDF Library for Delphi već uzima u obzir pomak pozicije koji je prvi, neuspjeli pokušaj mogao napraviti

Uklapanje ponovnog pokušaja lozinke u zaslon za prihvat dokumenata

Tijek prihvata dokumenta prirodno je mjesto za ovaj povratni poziv jer je upravo za takav problem OnPassword napravljen: datoteka dolazi izvan aplikacije, njezina lozinka unaprijed nije pouzdano poznata, a osoba koja unosi kandidate treba više od jednog pokušaja bez pisanja vlastite 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 kad operater otkaže, što ostavlja
  // LastErrorCode na 404 kako bi ga pozivatelj mogao prijaviti.
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)        // ovdje stiže samo provjereni dokument
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument prima Lib samo nakon što LoadFromFile vrati 1, što znači da se neka lozinka u toj razmjeni doista potvrdila prema rukovatelju kriptografijom datoteke; odbijeni pokušaj nikada ne doseže taj redak, kao ni napola otvoreni dokument. Nakon što se ovakav dokument potvrdi kao otvoren, vrijedi ponovno provjeriti njegove postavke zaštite, a ne pretpostaviti da lozinka koja je uspjela predstavlja cijelu sigurnosnu priču: revizija onoga što rječnik /Encrypt dokumenta stvarno navodi obuhvaća čitanje algoritma, revizije i bitova dozvola koje PDF Library for Delphi izlaže nakon učitavanja ovakve datoteke

Ponovni pokušaj lozinke također je uski primjer šire discipline koju PDF Library for Delphi primjenjuje kroz cijeli sloj raščlambe: datoteka koja se još nije dokazala ne dobiva povjerenje, bilo da je pitanje koja je lozinka otključava ili laže li polje duljine o veličini međuspremnika koji mu je potreban. Ojačavanje Pascal PDF parsera protiv zlonamjernih datoteka obrađuje drugu polovicu te discipline, dekodere koji svaki program fonta i slikovni tok u ulaznom PDF-u tretiraju kao neprijateljski ulaz, a ne kao ispravan dokument koji je samo zaboravio lozinku

OnPassword i petlja ponovnog pokušaja iza njega dio su standardne PDF Library for Delphi PDF biblioteke za Delphi i C++Builder, dostupni svugdje gdje su već dostupni LoadFromFile, LoadFromStream ili LoadFromString, bez zasebnog modula ili razine licence za dokument kojemu je potreban samo još jedan pokušaj lozinke