Техническа статия

Повторен опит с парола за шифрован PDF в Delphi

PDF Library for Delphi повтаря опита с грешна парола за шифрован PDF, като изхвърля TPDFDocument, който току-що се е провалил, и създава изцяло нов за следващия опит, управляван от обратно повикване OnPassword (TPDFlibPasswordEvent), което се изпълнява до шестнадесет пъти, преди да се откаже. Това е съзнателно отклонение от инстинкта, към който повечето Delphi разработчици посягат първо: да запазят обекта на документа, който вече седи в паметта, да му подадат коригирана парола и да заредят отново на място, вместо да започват от нулата. Цикълът за повторни опити на PDF Library for Delphi, добавен във v3.245.0, заема противоположната позиция по причини, специфични за онова, което оставя след себе си неуспешният опит с парола. Сценарият зад него е достатъчно обикновен, така че повечето Delphi приложения с интензивна работа с документи рано или късно го срещат: екран за приемане получава PDF, шифрован trailer принуждава диалог за парола, операторът сгрешава низа при въвеждане и диалогът се появява отново за втори опит. Нищо в това потребителско изживяване не е необичайно, така че кодът зад него трябва да приема повече от една кандидат-парола за същия файл, и трябва да го прави безопасно, без да пропуска състояние от отхвърления опит в следващия

Защо не може просто да повторите опита върху същия обект на документа?

Повторното използване на TPDFDocument между опити с парола не работи, защото неуспешният опит вече е разглобил този обект вътрешно, вместо да го остави в някакво паузирано, възобновимо състояние. Отварянето на шифрован PDF означава парсване на xref таблицата, изграждане на четец върху основния източник и конструиране на криптографски обработчик (crypt handler) от подадената парола, всичко това преди PDF Library for Delphi изобщо да може да провери дали тази парола е правилна. Когато паролата се окаже грешна, вътрешната процедура за зареждане на документа почиства четеца, xref таблицата и криптографския обработчик като част от неуспешния изход, точно както трябва, което означава, че там няма полуизграден парсер, който да чака коригирана парола при второ извикване. Прекарайте същия обект през още един опит за зареждане въпреки това и режимът на отказ е точно от онзи вид, който е мъчителен за отстраняване: грешка изплува от вътрешно състояние, изградено за различен, вече провален разбор, без нищо в нея да сочи очевидно обратно към паролата три извиквания нагоре по веригата. PDF Library for Delphi избягва целия клас проблеми, като никога не се опитва да възстанови обект на документ, след като той не е успял да се отвори; всеки опит получава документ, който никога не е виждал грешна парола, включително четец и xref таблица

Как обратното повикване OnPassword иска следващата парола?

TPDFlibPasswordEvent е типът обратно повикване, който PDF Library for Delphi извиква чрез TPDFlib.LoadFromFile, LoadFromStream и LoadFromString всеки път, когато току-що изпробваната парола се окаже грешна, и подава на обработчика три неща: кой опит предстои да се изпълни, параметър Password, който да бъде презаписан със следващия кандидат, и флаг Retry, който по подразбиране е false

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Паролата, подадена в първоначалното извикване на LoadFromFile, се брои за опит едно, така че първия път, когато OnPassword изобщо се задейства, AttemptNumber пристига като 2. Оставете Retry незададен и зареждането се проваля чисто с LastErrorCode 404; задайте го на true и PDF Library for Delphi опитва отново с това, което обработчикът току-що е записал в Password

Вътре в цикъла за повторни опити: нов TPDFDocument за всеки опит

Вътрешно PDF Library for Delphi отговаря на въпроса за жизнения цикъл на обекта по един и същ начин за LoadFromFile, LoadFromStream и LoadFromString: всеки опит, включително първият, конструира нов TPDFDocument, прекарва го през пълната последователност на отваряне с паролата, която този опит използва, и запазва обекта само ако паролата се потвърди. TPDFDocument на отхвърлен опит се освобождава незабавно, като повлича със себе си своя четец, xref таблица и криптографски обработчик, а следващият опит започва отначало с обект, който няма никаква история

Блок-схема на PDF Library for Delphi за цикъла за повторни опити с парола, в който всеки опит конструира нов TPDFDocument, отхвърлен опит освобождава своя четец, xref таблица и криптографски обработчик, а обратното повикване OnPassword решава дали да се изпълни още един опит или зареждането да се провали с LastErrorCode 404
Провален кандидат никога не оцелява в цикъла: документът му се освобождава със състоянието на парсера все още наполовина изградено. Задаването на Retry записва следващия кандидат в Password и повтаря с обект, който никога не е виждал грешна парола
// Опростен откъс от вътрешността на LoadFromFile: всеки опит получава
// документ, който никога не е виждал вече отхвърлена парола. FileName,
// AttemptNumber и AttemptPassword идват от обхващащия метод.
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);            // предаваме потвърдения документ на
        Doc := nil;                 // колекцията на извикващия; прескачаме Free по-долу
      end;
    Finally
      Doc.Free;                     // четецът, xref таблицата и криптографският
    End;                            // обработчик на отхвърления опит се разглобяват тук
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // успех или отказ, несвързан с паролата: спираме
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Този ред Doc := nil точно преди блока Finally е целият договор за жизнения цикъл на обекта в едно-единствено изречение. Документ, който се проваля, отнася полуизграденото си състояние на парсера в гроба със себе си, по замисъл, а документ, който успява, е единственият, който някога се добавя към FDocs, колекцията, която TPDFlib поддържа за всеки документ, отворен от извикващия. Нищо от отхвърления опит не е видимо извън цикъла за повторни опити: нито полуинициализиран четец, нито остарял брой страници, нито криптографски обработчик, изграден от грешен ключ

Колко пъти PDF Library for Delphi ще повтори опита с грешна парола?

PDF Library for Delphi допуска общо шестнадесет опита за едно извикване на LoadFromFile, LoadFromStream или LoadFromString, като паролата, подадена в самото извикване, се брои за опит едно. OnPassword се задейства само за опити от втори до шестнадесети, което ограничава обратното повикване до петнадесет извиквания; поискайте седемнадесети опит и PDF Library for Delphi отказва, без дори да извика обработчика. Оставете Retry на стойността му по подразбиране false в който и да е момент, или изчерпете всичките шестнадесет опита без правилна парола, и LoadFromFile връща 0 с LastErrorCode, зададен на 404, кодът на PDF Library for Delphi за отхвърлена парола. Ограничението съществува по причини отвъд подредеността: неограничен цикъл за повторни опити е лесен начин една сгрешена парола да се превърне в случаен denial-of-service срещу нишката, която изпълнява зареждането, особено когато обработчикът е свързан с нещо автоматизирано, като списък с вече виждани пароли, а не с човек, който щрака през диалог. PDF Library for Delphi зачита и Abort, извикан върху инстанцията TPDFlib отвътре в обработчика, тъй като Sender пристига като същия този обект, полезно зад бутон Cancel в диалог за парола, и спира цикъла за повторни опити при следващата проверка, независимо на какво е бил зададен Retry. Зареждане, което се проваля по причина, различна от грешна парола, например повредена xref таблица, никога изобщо не влиза в цикъла за повторни опити: PDF Library for Delphi докладва LastErrorCode 401 и спира след първия опит, защото никакъв брой предположения за парола не поправя структурно повреден файл

PDF Library for Delphi: лента на опитите от едно до шестнадесет, показваща OnPassword, задействан от втория опит нататък, седемнадесети опит, отказан без извикване на обработчика, и отделните изходи за успех, LastErrorCode 404 и LastErrorCode 401
Само паролата, подадена в самото извикване за зареждане, се брои за опит едно, а обратното повикване никога не се изпълнява повече от петнадесет пъти на зареждане. Структурно повреден файл заобикаля цикъла изцяло и докладва 401 след един-единствен опит

Цикълът за повторни опити работи ли еднакво за файлове, потоци и низове?

Обратното повикване OnPassword и ограничението от шестнадесет опита се държат идентично при LoadFromFile, LoadFromStream и LoadFromString, макар че трите входни точки задържат източника си по различен начин между опитите. Файлов път е евтин за повторно посещение, тъй като всеки опит просто отваря отново именувания файл, а източник низ вече седи в паметта като собствено копие на извикващия, така че нито един от двата не се нуждае от помощ от извикващия между опитите. Поток, подаден от извикващия, е единственият случай, върху който си струва да се спрете: LoadFromStream премества този поток обратно на позиция нула и го копира вътрешно преди първия опит за парсване, така че всеки следващ опит, и новоконструираният TPDFDocument зад него, се възпроизвежда от това вътрешно копие, а не от мястото, където провален разбор е оставил позицията на потока. Подайте на PDF Library for Delphi TFileStream или TMemoryStream за документ, защитен с парола, и няма нужда да го превъртате между повторните опити; PDF Library for Delphi вече отчита позиция, която първи, провален опит може да е преместил

Вграждане на повторния опит с парола в екран за приемане на документи

Работен процес за приемане на документи е естественият дом на това обратно повикване, защото това е точно формата на проблема, за чието решаване е създаден OnPassword: файл пристига отвън в приложението, паролата му не е известна със сигурност предварително, а човекът, подаващ кандидати, се нуждае от повече от едно предположение, без околният код да пише собствен цикъл за повторни опити около LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber брои от 2: вече изпробваната парола беше опит 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry е False, когато операторът откаже, което оставя
  // LastErrorCode на 404, за да го докладва извикващият.
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)        // само потвърден документ стига дотук
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument получава Lib само след като LoadFromFile е върнал 1, което означава, че някоя парола в този обмен действително се е потвърдила срещу криптографския обработчик на файла; отхвърлен опит никога не стига до този ред, нито пък полуотворен документ. Това, което следва, след като документ като този е потвърден като отворен, заслужава втори поглед към настройките му за защита, а не предположение, че паролата, която е проработила, е цялата история за сигурността: одитът на това, което речникът /Encrypt на документа действително декларира, покрива четенето на алгоритъма, ревизията и битовете за разрешения, които PDF Library for Delphi излага, след като такъв файл се зареди

Повторният опит с парола е и тесен случай на по-широка дисциплина, която PDF Library for Delphi прилага в целия си слой за парсване: файл, който още не се е доказал, не получава презумпция за невинност, независимо дали въпросът е коя парола го отключва или дали поле за дължина вътре в него лъже за размера на буфера, от който се нуждае. Укрепването на Pascal PDF парсер срещу злонамерени файлове покрива другата половина на тази дисциплина, декодерите, които третират всяка шрифтова програма и всеки поток с изображение във входящ PDF като враждебен вход, а не като добре оформен документ, който просто е забравил паролата си

OnPassword и цикълът за повторни опити зад него са част от стандартната PDF библиотека PDF Library for Delphi за Delphi и C++Builder, налична навсякъде, където вече са LoadFromFile, LoadFromStream или LoadFromString, без да е нужен отделен модул или лицензионно ниво за документ, който просто има нужда от второ предположение за паролата си