Technický článek

Opakování hesel šifrovaných PDF v Delphi s PDFlibPas

PDFlibPas opakuje špatné heslo na šifrovaném PDF tak, že zahodí TPDFDocument, který právě selhal, a vytvoří úplně nový pro další pokus, řízený zpětným voláním OnPassword (TPDFlibPasswordEvent), které běží až šestnáctkrát, než se vzdá. Je to záměrný odklon od instinktu, po kterém sáhne většina vývojářů v Delphi jako první: podržet objekt dokumentu, který už sedí v paměti, podat mu opravené heslo, a znovu načíst na místě místo začínání od nuly. Smyčka opakování PDFlibPas, přidaná v v3.245.0, zaujímá opačnou pozici, z důvodů specifických pro to, co pokus se špatným heslem zanechá za sebou. Scénář za tím je dost obyčejný na to, aby na něj většina aplikací v Delphi náročných na dokumenty nakonec narazila: obrazovka příjmu přijme PDF, šifrovaný trailer vynutí dialog hesla, operátor se překlepne, a dialog se objeví znovu na druhý pokus. Nic na tomto zážitku uživatele není neobvyklé, takže kód za ním musí přijmout víc než jedno kandidátní heslo pro stejný soubor, a musí to udělat bezpečně, bez úniku stavu z odmítnutého pokusu do toho, který následuje

Proč nemůžete prostě opakovat na stejném objektu dokumentu?

Znovupoužití TPDFDocument napříč pokusy o heslo nefunguje, protože selhaný pokus už tento objekt interně strhl místo toho, aby jej ponechal v nějakém pozastaveném, obnovitelném stavu. Otevření šifrovaného PDF znamená parsování tabulky křížového odkazu, sestavení čtenáře nad podkladovým zdrojem, a konstrukci obsluhy šifrování z jakéhokoli podaného hesla, to vše dřív, než PDFlibPas vůbec dokáže otestovat, zda je toto heslo správné. Když se heslo ukáže být špatné, interní rutina načítání dokumentu vyčistí čtenáře, tabulku křížového odkazu, a obsluhu šifrování jako součást selhání, přesně jak by měla, což znamená, že tam nesedí žádný napůl postavený parser čekající na opravené heslo při druhém volání. Prožeňte přesto stejný objekt dalším pokusem o načtení a režim selhání je přesně ten druh, který je bídný na ladění: chyba se vynoří z interního stavu postaveného pro jiný, už selhaný parsing, s ničím na ní zjevně ukazujícím zpátky na heslo o tři volání proti proudu. PDFlibPas se celé této třídě problému vyhýbá tím, že se nikdy nesnaží zotavit objekt dokumentu, jakmile jednou selže při otevření; každý pokus dostane dokument, který nikdy neviděl špatné heslo, čtenáře a tabulku křížového odkazu nevyjímaje

Jak zpětné volání OnPassword žádá o další heslo?

TPDFlibPasswordEvent je typ zpětného volání, který PDFlibPas vyvolá přes TPDFlib.LoadFromFile, LoadFromStream, a LoadFromString kdykoli se heslo, které bylo právě zkusené, ukáže být špatné, a podá obsluze tři věci: který pokus se právě chystá běžet, parametr Password k přepsání dalším kandidátem, a příznak Retry, který ve výchozím stavu je false

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Heslo předané do původního volání LoadFromFile se počítá jako pokus jedna, takže poprvé, kdy se OnPassword vůbec vyvolá, AttemptNumber dorazí jako 2. Ponechte Retry nenastavené a načtení čistě selže s LastErrorCode 404; nastavte jej na true a PDFlibPas to zkusí znovu s tím, co obsluha právě zapsala do Password

Uvnitř smyčky opakování: nový TPDFDocument pro každý pokus

Interně PDFlibPas odpovídá na otázku životního cyklu objektu stejným způsobem pro LoadFromFile, LoadFromStream, a LoadFromString: každý pokus, včetně prvního, konstruuje čerstvý TPDFDocument, prožene jej kompletní sekvencí otevření s jakýmkoli heslem, které tento pokus používá, a objekt si podrží jen tehdy, když se heslo ověří. TPDFDocument odmítnutého pokusu se okamžitě uvolní, strhne s sebou čtenáře, tabulku křížového odkazu, a obsluhu šifrování, a další pokus začíná znovu s objektem, který nemá žádnou historii vůbec

// 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 řádek Doc := nil těsně před blokem Finally je celá smlouva životního cyklu objektu v jednom příkazu. Dokument, který selže, si nese svůj napůl postavený stav parseru do hrobu, záměrně, a dokument, který uspěje, je jediný, který se kdy přidá do FDocs, kolekce, kterou TPDFlib drží pro každý dokument, který volající má otevřený. Nic z odmítnutého pokusu není viditelné zvenčí smyčky opakování: žádný napůl inicializovaný čtenář, žádný zastaralý počet stránek, žádná obsluha šifrování postavená ze špatného klíče

Kolikrát bude PDFlibPas opakovat špatné heslo?

PDFlibPas povoluje celkem šestnáct pokusů proti jednomu volání LoadFromFile, LoadFromStream, nebo LoadFromString, počítá heslo předané do samotného volání jako pokus jedna. OnPassword se vyvolá jen pro pokusy dvě až šestnáct, což omezuje zpětné volání na patnáct vyvolání; požádejte o sedmnáctý pokus a PDFlibPas odmítne, aniž by dokonce obsluhu vyvolal. Ponechte Retry na jeho výchozí hodnotě false kdykoli, nebo vyčerpejte všech šestnáct pokusů bez správného hesla, a LoadFromFile vrátí 0 s LastErrorCode nastaveným na 404, kód PDFlibPas pro odmítnuté heslo. Strop existuje z důvodů nad rámec úhlednosti: neomezená smyčka opakování je snadný způsob, jak proměnit jedno překlepnuté heslo na náhodné odepření služby proti jakémukoli vláknu, které spouští načtení, obzvlášť jakmile je obsluha zapojená k něčemu automatizovanému, jako seznam dříve viděných hesel, místo člověka proklikávajícího dialog. PDFlibPas také respektuje Abort zavolané na instanci TPDFlib zevnitř obsluhy, protože Sender dorazí jako tentýž objekt, užitečné za tlačítkem Zrušit na dialogu hesla, a zastaví smyčku opakování na další kontrole bez ohledu na to, na co bylo Retry nastaveno. Načtení, které selže z jiného důvodu než špatného hesla, poškozené tabulky křížového odkazu například, do smyčky opakování vůbec nevstoupí: PDFlibPas nahlásí LastErrorCode 401 a zastaví se po prvním pokusu, protože žádný počet hádaných hesel neopraví strukturálně rozbitý soubor

Funguje smyčka opakování stejně pro soubory, proudy a řetězce?

Zpětné volání OnPassword a strop šestnácti pokusů se chovají identicky napříč LoadFromFile, LoadFromStream, a LoadFromString, přestože tyto tři vstupní body drží svůj zdroj mezi pokusy odlišně. Cesta k souboru je levné navštívit znovu, protože každý pokus prostě znovu otevře pojmenovaný soubor, a zdroj řetězce už sedí v paměti jako vlastní kopie volajícího, takže ani jeden nepotřebuje žádnou pomoc od volajícího mezi pokusy. Proud dodaný volajícím je jediný případ, který stojí za pozastavení: LoadFromStream posune tento proud zpátky na pozici nula a interně jej zkopíruje ještě před prvním pokusem o parsování, takže každý následující pokus, a čerstvě zkonstruovaný TPDFDocument za ním, přehrává z této interní kopie místo z toho, kde selhané parsování ponechalo pozici proudu. Podejte PDFlibPas TFileStream nebo TMemoryStream pro dokument chráněný heslem a není potřeba jej mezi opakováními přetáčet zpátky; PDFlibPas už počítá s pozicí, kterou první, selhaný pokus mohl posunout

Zapojení opakování hesla do obrazovky příjmu dokumentů

Pracovní postup příjmu dokumentů je přirozený domov pro toto zpětné volání, protože je to přesně tvar problému, pro který bylo OnPassword postaveno: soubor dorazí mimo aplikaci, jeho heslo není předem známé s jistotou, a osoba dodávající kandidáty potřebuje víc než jeden hádaný pokus bez toho, aby okolní kód psal vlastní smyčku opakování kolem 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 jen tehdy, když se LoadFromFile vrátilo 1, což znamená, že se nějaké heslo v této výměně skutečně ověřilo proti obsluze šifrování souboru; odmítnutý pokus se na tento řádek nikdy nedostane, a stejně tak ani napůl otevřený dokument. Co následuje, jakmile je takový dokument potvrzen jako otevřený, stojí za druhý pohled na jeho nastavení ochrany místo předpokladu, že heslo, které fungovalo, je celý bezpečnostní příběh: audit toho, co slovník /Encrypt dokumentu skutečně deklaruje popisuje čtení algoritmu, revize, a bitů oprávnění, které PDFlibPas vystavuje, jakmile se takový soubor načte

Opakování hesla je také úzká instance širší disciplíny, kterou PDFlibPas aplikuje napříč svou vrstvou parsování: soubor, který se ještě neosvědčil, nedostává výhodu pochybnosti, ať je otázka, které heslo jej odemyká, nebo zda pole délky uvnitř lže o velikosti bufferu, který potřebuje. Zpevňování parseru PDF v Pascalu proti škodlivým souborům popisuje druhou polovinu této disciplíny, dekodéry, které berou každý program fontu a proud obrázku v příchozím PDF jako nepřátelský vstup místo dobře formovaného dokumentu, který si prostě zapomněl heslo

OnPassword a smyčka opakování za ním jsou součástí standardní knihovny PDF PDFlibPas pro Delphi a C++Builder, dostupné kdekoli, kde už jsou LoadFromFile, LoadFromStream, nebo LoadFromString, bez potřeby samostatného modulu nebo licenční úrovně pro dokument, který jen potřebuje druhý hádaný pokus o své heslo