Odborný článok

Opakované zadávanie hesla pre šifrované PDF v Delphi s PDFlibPas

PDFlibPas opakuje nesprávne heslo k šifrovanému PDF tak, že zahodí TPDFDocument, ktorý práve zlyhal, a pre ďalší pokus vytvorí úplne nový, riadený udalosťou OnPassword (TPDFlibPasswordEvent), ktorá beží až do šestnástich pokusov, kým sa vzdá. Ide o zámerné odklonenie sa od inštinktu, po ktorom siahne väčšina vývojárov v Delphi ako prvom rade: ponechať objekt dokumentu už sediaci v pamäti, dodať mu opravené heslo a znovu ho načítať na mieste namiesto začínania od nuly. Cyklus opakovania v PDFlibPas, pridaný vo verzii v3.245.0, zaujíma opačné stanovisko, z dôvodov špecifických pre to, čo neúspešný pokus o heslo zanecháva za sebou. Scenár za tým je natoľko bežný, že naň narazí väčšina aplikácií v Delphi náročných na dokumenty skôr či neskôr: obrazovka na príjem prijme PDF, šifrovaný trailer vynúti dialóg na zadanie hesla, operátor sa preklepne a dialóg sa objaví znova na druhý pokus. Nič na tomto používateľskom zážitku nie je nezvyčajné, takže kód za ním musí prijať viac než jedno kandidátske heslo pre ten istý súbor, a musí to robiť bezpečne, bez toho, aby stav z odmietnutého pokusu unikol do toho nasledujúceho

Prečo nemožno jednoducho opakovať pokus na tom istom objekte dokumentu?

Opätovné použitie TPDFDocument naprieč pokusmi o heslo nefunguje, pretože neúspešný pokus tento objekt už interne rozobral namiesto toho, aby ho ponechal v nejakom pozastavenom, obnoviteľnom stave. Otvorenie šifrovaného PDF znamená spracovať tabuľku krížových odkazov, zostaviť čítač nad podkladovým zdrojom a zostrojiť obslužný modul šifrovania z akéhokoľvek dodaného hesla, a to všetko ešte predtým, než PDFlibPas vôbec dokáže overiť, či je toto heslo správne. Keď sa heslo ukáže ako nesprávne, interná načítacia rutina dokumentu vyčistí čítač, tabuľku krížových odkazov a obslužný modul šifrovania v rámci riadneho zlyhania, presne ako má, čo znamená, že tam nesedí žiadny napoly zostavený parser čakajúci na opravené heslo pri druhom volaní. Prehnať ten istý objekt aj tak cez ďalší pokus o načítanie znamená vyvolať presne ten druh zlyhania, ktorý sa mizerne ladí: chyba sa vynorí z interného stavu vybudovaného pre iné, už zlyhané spracovanie, pričom nič na nej zjavne neukazuje späť na heslo o tri volania vyššie. PDFlibPas sa tejto celej triede problémov vyhýba tým, že sa nikdy nesnaží obnoviť objekt dokumentu po tom, čo sa mu nepodarilo otvoriť; každý pokus dostane dokument, ktorý ešte nikdy nevidel nesprávne heslo, vrátane čítača a tabuľky krížových odkazov

Ako udalosť OnPassword žiada o ďalšie heslo?

TPDFlibPasswordEvent je typ udalosti, ktorú PDFlibPas vyvoláva cez TPDFlib.LoadFromFile, LoadFromStream a LoadFromString vždy, keď sa práve vyskúšané heslo ukáže ako nesprávne, a odovzdáva obslužnej rutine tri veci: ktorý pokus sa práve chystá bežať, parameter Password na prepísanie ďalším kandidátom a príznak Retry, ktorý má predvolenú hodnotu false

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Heslo odovzdané do pôvodného volania LoadFromFile sa počíta ako pokus číslo jeden, takže pri prvom vôbec spustení OnPassword príde AttemptNumber ako 2. Ponechajte Retry nenastavené a načítanie čisto zlyhá s LastErrorCode 404; nastavte ho na true a PDFlibPas skúsi znova s tým, čo obslužná rutina práve zapísala do Password

Vnútri cyklu opakovania: nový TPDFDocument pre každý pokus

Interne PDFlibPas odpovedá na otázku životného cyklu objektu rovnako pre LoadFromFile, LoadFromStream a LoadFromString: každý pokus, vrátane prvého, zostrojí čerstvý TPDFDocument, prežene ho celou sekvenciou otvárania s heslom, ktoré daný pokus používa, a objekt si ponechá jedine vtedy, ak sa heslo overí. TPDFDocument z odmietnutého pokusu sa okamžite uvoľní, a spolu s ním padne jeho čítač, tabuľka krížových odkazov a obslužný modul šifrovania, a ďalší pokus začína nanovo s objektom, ktorý nemá vôbec žiadnu históriu

// 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;

Ten riadok Doc := nil tesne pred blokom Finally je celá zmluva o životnom cykle objektu v jedinom príkaze. Dokument, ktorý zlyhá, si zámerne odnáša svoj napoly zostavený stav parsera do hrobu, a dokument, ktorý uspeje, je jediný, ktorý sa kedy pridá do FDocs, kolekcie, ktorú TPDFlib udržiava pre každý dokument, ktorý má volajúci otvorený. Nič z odmietnutého pokusu nie je viditeľné zvonka cyklu opakovania: žiadny napoly inicializovaný čítač, žiadny zastaralý počet strán, žiadny obslužný modul šifrovania zostrojený z nesprávneho kľúča

Koľkokrát PDFlibPas zopakuje pokus s nesprávnym heslom?

PDFlibPas povoľuje celkovo šestnásť pokusov pri jednom volaní LoadFromFile, LoadFromStream alebo LoadFromString, pričom heslo odovzdané priamo do volania sa počíta ako prvý pokus. OnPassword sa spustí iba pre pokusy dva až šestnásť, čo obmedzuje udalosť na pätnásť vyvolaní; požiadajte o sedemnásty pokus a PDFlibPas ho odmietne bez toho, aby vôbec vyvolal obslužnú rutinu. Ponechajte Retry na jej predvolenej hodnote false v ktoromkoľvek okamihu, alebo vyčerpajte všetkých šestnásť pokusov bez správneho hesla, a LoadFromFile vráti 0 s LastErrorCode nastaveným na 404, kód PDFlibPas pre odmietnuté heslo. Tento strop existuje z dôvodov presahujúcich len poriadok: neobmedzený cyklus opakovania je ľahký spôsob, ako z jedného preklepnutého hesla omylom urobiť odopretie služby voči akémukoľvek vláknu, ktoré načítanie beží, obzvlášť keď je obslužná rutina prepojená s niečím automatizovaným, ako je zoznam predtým videných hesiel, namiesto človeka preklikávajúceho sa dialógom. PDFlibPas tiež rešpektuje volanie Abort na inštancii TPDFlib zvnútra obslužnej rutiny, keďže Sender prichádza ako ten istý objekt, čo je užitočné za tlačidlom Zrušiť na dialógu hesla, a zastaví cyklus opakovania pri ďalšej kontrole bez ohľadu na to, na čo bolo Retry nastavené. Načítanie, ktoré zlyhá z iného dôvodu než nesprávneho hesla, napríklad poškodená tabuľka krížových odkazov, do cyklu opakovania vôbec nevstúpi: PDFlibPas nahlási LastErrorCode 401 a zastaví sa po prvom pokuse, pretože žiadny počet hádaní hesla neopraví štrukturálne poškodený súbor

Funguje cyklus opakovania rovnako pre súbory, streamy a reťazce?

Udalosť OnPassword a strop šestnástich pokusov sa správajú identicky naprieč LoadFromFile, LoadFromStream a LoadFromString, hoci tieto tri vstupné body medzi pokusmi zaobchádzajú so svojím zdrojom odlišne. Cesta k súboru sa dá lacno znovu navštíviť, keďže každý pokus jednoducho znovu otvorí pomenovaný súbor, a zdroj typu reťazec už sedí v pamäti ako vlastná kópia volajúceho, takže ani jeden z nich medzi pokusmi nepotrebuje žiadnu pomoc od volajúceho. Stream dodaný volajúcim je jediný prípad, pri ktorom sa oplatí zastaviť: LoadFromStream posunie tento stream späť na pozíciu nula a interne ho skopíruje ešte pred prvým pokusom o spracovanie, takže každý nasledujúci pokus, a čerstvo zostrojený TPDFDocument za ním, prehráva z tejto internej kópie namiesto z toho miesta, kde neúspešné spracovanie ponechalo pozíciu streamu. Podajte PDFlibPas TFileStream alebo TMemoryStream pre heslom chránený dokument a nie je potrebné ho medzi pokusmi previjať späť; PDFlibPas už berie do úvahy pozíciu, ktorú mohol prvý, neúspešný pokus posunúť

Zapojenie opakovania hesla do obrazovky na príjem dokumentov

Pracovný postup príjmu dokumentov je prirodzeným miestom pre túto udalosť, pretože ide presne o tvar problému, na riešenie ktorého bol OnPassword vytvorený: súbor prichádza zvonka aplikácie, jeho heslo nie je vopred isto známe, a osoba dodávajúca kandidátov potrebuje viac než jeden pokus bez toho, aby okolitý kód písal vlastný cyklus opakovania okolo 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 dostane Lib jedine vtedy, keď LoadFromFile vrátilo 1, čo znamená, že v danej výmene sa nejaké heslo skutočne overilo voči obslužnému modulu šifrovania súboru; odmietnutý pokus sa na tento riadok nikdy nedostane, ani napoly otvorený dokument. To, čo nasleduje, akonáhle je takýto dokument potvrdene otvorený, si zaslúži druhý pohľad na jeho nastavenia ochrany namiesto predpokladu, že heslo, ktoré zabralo, je celý bezpečnostný príbeh: audit toho, čo slovník /Encrypt dokumentu skutočne deklaruje pokrýva čítanie algoritmu, revízie a bitov oprávnení, ktoré PDFlibPas vystavuje po načítaní takéhoto súboru

Opakovanie hesla je zároveň úzkym príkladom širšej disciplíny, ktorú PDFlibPas uplatňuje naprieč celou svojou vrstvou spracovania: súbor, ktorý sa ešte nepreukázal, nedostáva žiadnu výhodu pochybnosti, či už ide o otázku, ktoré heslo ho odomkne, alebo o to, či pole dĺžky vnútri neho neklame o veľkosti bufferu, ktorý potrebuje. Spevnenie parsera PDF v Pascale proti škodlivým súborom pokrýva druhú polovicu tejto disciplíny, dekodéry, ktoré zaobchádzajú s každým programom písma a obrazovým streamom v prichádzajúcom PDF ako s nepriateľským vstupom, nie ako s riadne zostaveným dokumentom, ktorý si len zabudol heslo

OnPassword a cyklus opakovania za ňou sú súčasťou štandardnej PDF knižnice PDFlibPas pre Delphi a C++Builder, dostupnej všade tam, kde sú už LoadFromFile, LoadFromStream alebo LoadFromString, bez potreby samostatného modulu alebo licenčnej úrovne pre dokument, ktorý potrebuje len druhý pokus o heslo