Műszaki cikk

Titkosított PDF-jelszavak újrapróbálása Delphiben a PDFlibPasszal

A PDFlibPas úgy próbálja újra a rossz jelszót egy titkosított PDF-en, hogy eldobja azt a TPDFDocument-et, amelyik épp elbukott, és teljesen újat hoz létre a következő próbálkozáshoz, egy OnPassword callback (TPDFlibPasswordEvent) által vezérelve, amely akár tizenhat próbán keresztül fut, mielőtt feladná. Ez egy szándékos eltérés attól az ösztöntől, amihez a legtöbb Delphi-fejlesztő először nyúlna: tartsd meg a már memóriában ülő dokumentumobjektumot, táplálj neki egy javított jelszót, és tölts be újra helyben ahelyett hogy elölről kezdenéd. A PDFlibPas újrapróbálási ciklusa, amelyet v3.245.0-ban adtak hozzá, az ellenkező álláspontot foglalja el, olyan okokból, amelyek konkrétan arra vonatkoznak, mit hagy maga után egy elbukott jelszó-próbálkozás. A mögötte álló forgatókönyv elég hétköznapi ahhoz, hogy a legtöbb dokumentum-nehéz Delphi-alkalmazás előbb-utóbb belefusson: egy beviteli képernyő elfogad egy PDF-et, egy titkosított lezáró (trailer) kikényszerít egy jelszó-párbeszédablakot, a kezelő elgépeli a sztringet, és a párbeszédablak ismét megjelenik egy második próbához. Semmi ebben a felhasználói élményben nem szokatlan, így a mögötte lévő kódnak el kell fogadnia egynél több jelölt jelszót ugyanahhoz a fájlhoz, és biztonságosan kell megtennie, anélkül hogy állapotot szivárogtatna az elutasított próbálkozásból abba, amely követi

Miért nem lehet egyszerűen újrapróbálni ugyanazon a dokumentumobjektumon?

Egy TPDFDocument újrahasznosítása jelszó-próbálkozások között nem működik, mert egy elbukott próbálkozás már belsőleg lebontotta azt az objektumot, ahelyett hogy valamilyen szüneteltetett, folytatható állapotban hagyná. Egy titkosított PDF megnyitása azt jelenti, hogy elemzi a kereszthivatkozás-táblát, felépít egy olvasót az alapul szolgáló forrás fölé, és felépít egy titkosító-kezelőt bármilyen jelszóból, amit megadtak, mindezt azelőtt hogy a PDFlibPas egyáltalán tesztelhetné, helyes-e az a jelszó. Amikor a jelszó rossznak bizonyul, a dokumentum belső betöltő rutinja megtisztítja az olvasót, a kereszthivatkozás-táblát, és a titkosító-kezelőt a bukás részeként, pontosan úgy, ahogyan kellene, ami azt jelenti, hogy nincs félig-felépített elemző, amely ott ülne, egy javított jelszóra várva egy második híváskor. Vezesd ugyanazt az objektumot egy másik betöltési próbálkozáson keresztül mégis, és a hibamód pontosan az a fajta, amit gyötrelmes debuggolni: egy hiba egy olyan belső állapotból bukkan fel, amit egy más, már elbukott elemzéshez építettek, semmivel, ami nyilvánvalóan visszamutatna a jelszóra három hívással upstream. A PDFlibPas elkerüli a teljes problémaosztályt azáltal, hogy soha nem próbál helyreállítani egy dokumentumobjektumot, miután az elbukott a megnyitásban; minden próbálkozás egy olyan dokumentumot kap, amely soha nem látott rossz jelszót, az olvasót és a kereszthivatkozás-táblát is beleértve

Hogyan kéri az OnPassword callback a következő jelszót?

A TPDFlibPasswordEvent az a callback-típus, amit a PDFlibPas meghív a TPDFlib.LoadFromFile-on, LoadFromStream-en, és LoadFromString-en keresztül, valahányszor az épp kipróbált jelszó rossznak bizonyul, és három dolgot ad át a kezelőnek: melyik próbálkozás fog lefutni, egy Password paramétert felülíráshoz a következő jelölttel, és egy Retry jelzőt, amely alapértelmezetten hamis

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Az eredeti LoadFromFile hívásba átadott jelszó első próbálkozásnak számít, így amikor az OnPassword egyáltalán először kiváltódik, az AttemptNumber 2-ként érkezik. Hagyd a Retry-t beállítatlanul, és a betöltés tisztán elbukik 404-es LastErrorCode-dal; állítsd igazra, és a PDFlibPas újra megpróbálja azzal, amit a kezelő éppen beírt a Password-ba

Az újrapróbálási ciklus belseje: friss TPDFDocument minden próbálkozáshoz

Belsőleg a PDFlibPas ugyanúgy válaszolja meg az objektum-élettartam kérdését a LoadFromFile-hoz, a LoadFromStream-hez, és a LoadFromString-hez: minden próbálkozás, az elsőt is beleértve, egy friss TPDFDocument-et konstruál, végigfuttatja a teljes megnyitási sorozaton, azzal a jelszóval, amit az a próbálkozás használ, és csak akkor tartja meg az objektumot, ha a jelszó ellenőrzésre kerül. Egy elutasított próbálkozás TPDFDocument-je azonnal felszabadul, magával rántva olvasóját, kereszthivatkozás-tábláját, és titkosító-kezelőjét, és a következő próbálkozás elölről kezd egy olyan objektummal, amelynek egyáltalán nincs története

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

Az a Doc := nil sor, közvetlenül a Finally blokk előtt, a teljes objektum-élettartam-szerződés egyetlen utasításban. Egy dokumentum, amely elbukik, tervezésénél fogva magával viszi félig-felépített elemzőállapotát a sírba, és egy dokumentum, amely sikeres, az egyetlen, amit valaha is hozzáadnak az FDocs-hoz, ahhoz a gyűjteményhez, amit a TPDFlib tart minden dokumentumhoz, amit a hívó nyitva tart. Semmi egy elutasított próbálkozásból nem látható kívülről az újrapróbálási cikluson: nem egy félig-inicializált olvasó, nem egy elavult oldalszám, nem egy titkosító-kezelő, amit a rossz kulcsból építettek

Hányszor próbálja újra a PDFlibPas egy rossz jelszót?

A PDFlibPas összesen tizenhat próbálkozást engedélyez egyetlen LoadFromFile, LoadFromStream, vagy LoadFromString hívás ellen, a hívásba magába átadott jelszót első próbálkozásnak számolva. Az OnPassword csak a második-tizenhatodik próbálkozásokhoz váltódik ki valaha, ami tizenöt meghívásra korlátozza a callback-et; kérj egy tizenhetediket, és a PDFlibPas elutasítja, anélkül hogy egyáltalán meghívná a kezelőt. Hagyd a Retry-t az alapértelmezett hamis értéken bármikor, vagy fogyaszd el mind a tizenhat próbálkozást helyes jelszó nélkül, és a LoadFromFile 0-t ad vissza, LastErrorCode-dal 404-re állítva, a PDFlibPas kódja egy elutasított jelszóhoz. A plafon rendezettségen túli okokból is létezik: egy korlátlan újrapróbálási ciklus könnyű mód arra, hogy egy elgépelt jelszóból véletlen szolgáltatásmegtagadás legyen bármely szál ellen, amely a betöltést futtatja, különösen amint egy kezelő valami automatizálthoz van bekötve, mint egy korábban látott jelszavak listája, nem egy ember, aki átkattint egy párbeszédablakon. A PDFlibPas tiszteletben tartja az Abort-ot is, amit a TPDFlib-példányon hívnak meg a kezelőn belülről, mivel a Sender ugyanazon objektumként érkezik, hasznos egy Mégse gomb mögött egy jelszó-párbeszédablakon, és leállítja az újrapróbálási ciklust a következő ellenőrzésnél, függetlenül attól, mire állították a Retry-t. Egy betöltés, amely más okból bukik el, mint egy rossz jelszó, például egy sérült kereszthivatkozás-tábla, egyáltalán soha nem lép be az újrapróbálási ciklusba: a PDFlibPas 401-es LastErrorCode-ot jelent, és az első próbálkozás után megáll, mert semennyi jelszó-találgatás nem javít meg egy strukturálisan sérült fájlt

Ugyanúgy működik-e az újrapróbálási ciklus fájlokhoz, streamekhez, és sztringekhez?

Az OnPassword callback és a tizenhat-próbás plafon azonosan viselkedik a LoadFromFile, LoadFromStream, és LoadFromString között, bár a három belépési pont eltérően tartja meg forrását próbálkozások között. Egy fájlútvonalat olcsó újralátogatni, mivel minden próbálkozás egyszerűen újra megnyitja a nevezett fájlt, és egy sztring-forrás már memóriában ül a hívó saját másolataként, így egyiknek sincs szüksége semmilyen segítségre a hívótól próbálkozások között. Egy hívó által biztosított stream az az eset, amelynél érdemes megállni: a LoadFromStream visszakeresteti azt a streamet nulla pozícióba, és belsőleg lemásolja, mielőtt az első elemzési próbálkozás megtörténne, így minden azt követő próbálkozás, és a mögötte lévő, frissen konstruált TPDFDocument, arról a belső másolatról játszik vissza, nem onnan, ahol egy elbukott elemzés hagyta a stream pozícióját. Add át a PDFlibPas-nak egy TFileStream-et vagy TMemoryStream-et egy jelszóval védett dokumentumhoz, és nincs szükség visszatekerésre próbálkozások között; a PDFlibPas már számol azzal a pozícióval, amit egy első, elbukott próbálkozás mozgatott

Jelszó-újrapróbálás illesztése egy dokumentum-beviteli képernyőbe

Egy dokumentum-beviteli munkafolyamat a természetes otthona ennek a callback-nek, mert pontosan az a fajta probléma-alak, amelynek megoldására az OnPassword épült: egy fájl az alkalmazáson kívülről érkezik, jelszava előre nem ismert bizonyossággal, és a jelölteket megadó személynek több mint egy találgatásra van szüksége anélkül, hogy a környező kód saját újrapróbálási ciklust írna a LoadFromFile köré

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;

A RegisterIntakeDocument csak akkor kapja meg a Lib-et, ha a LoadFromFile 1-et adott vissza, ami azt jelenti, hogy valamilyen jelszó abban a cserében ténylegesen ellenőrizve lett a fájl titkosító-kezelője ellen; egy elutasított próbálkozás soha nem éri el azt a sort, és egy félig-nyitott dokumentum sem. Ami ezután következik, amint egy ilyen dokumentum megerősítetten nyitva van, érdemes egy második pillantás a védelmi beállításaira, ahelyett hogy feltételeznénk, a jelszó, amely működött, az egész biztonsági történet: annak auditálása, hogy egy dokumentum /Encrypt szótára ténylegesen mit deklarál tárgyalja az algoritmus, revízió, és jogosultsági bitek olvasását, amiket a PDFlibPas felfed, amint egy ilyen fájl betöltődik

A jelszó-újrapróbálás egy szűk esete is egy szélesebb fegyelemnek, amit a PDFlibPas a teljes elemzőrétegében alkalmaz: egy fájl, amely még nem bizonyította magát, nem kap kétség-előnyt, akár az a kérdés, melyik jelszó oldja fel, akár az, hogy egy hosszúságmező benne hazudik-e a szükséges puffer méretéről. Egy Pascal PDF-elemző megerősítése rosszindulatú fájlok ellen tárgyalja e fegyelem másik felét, azokat a dekódolókat, amelyek minden betűtípus-programot és képfolyamot egy bejövő PDF-ben ellenséges bemenetként kezelnek, nem egy jólformázott dokumentumként, amely csupán elfelejtette jelszavát

Az OnPassword és a mögötte lévő újrapróbálási ciklus a Delphihez és C++Builderhez készült szabványos PDFlibPas PDF-könyvtár részei, elérhető bárhol, ahol a LoadFromFile, LoadFromStream, vagy LoadFromString már elérhető, semmilyen külön modul vagy licencszint nélkül egy dokumentumhoz, amelynek csupán egy második találgatásra van szüksége jelszavához