Технічна стаття

Повторні спроби пароля зашифрованого PDF у Delphi з PDFlibPas

PDFlibPas повторює неправильний пароль зашифрованого PDF, відкидаючи TPDFDocument, що щойно провалився, і створюючи цілком новий для наступної спроби, керований колбеком OnPassword (TPDFlibPasswordEvent), що виконується до шістнадцяти разів, перш ніж здатися. Це навмисне відхилення від інстинкту, до якого більшість розробників Delphi звертаються першим: тримати об'єкт документа, що вже сидить у пам'яті, подати йому виправлений пароль і завантажити знову на місці замість того, щоб починати з нуля. Цикл повторних спроб PDFlibPas, доданий у v3.245.0, займає протилежну позицію, з причин, специфічних для того, що залишає невдала спроба пароля. Сценарій за цим достатньо звичайний, що більшість застосунків, насичених документами, на Delphi врешті-решт натрапляють на нього: екран прийому приймає PDF, зашифрований трейлер змушує з'явитися діалог пароля, оператор помиляється пальцем у рядку, і діалог з'являється знову для другої спроби. Ніщо в цьому досвіді користувача не незвичайне, тож код за ним мусить приймати більше однієї кандидатки пароля для того самого файлу, і мусить робити це безпечно, не витікаючи стан відхиленої спроби в наступну

Чому не можна просто повторити спробу на тому самому об'єкті документа?

Повторне використання TPDFDocument через спроби пароля не працює, бо невдала спроба вже зруйнувала цей об'єкт внутрішньо, а не залишила його в якомусь призупиненому, відновному стані. Відкриття зашифрованого PDF означає розбір таблиці перехресних посилань, побудову читача над базовим джерелом та конструювання обробника шифрування з будь-якого поданого пароля, і все це ще до того, як PDFlibPas взагалі може перевірити, чи цей пароль правильний. Коли пароль виявляється неправильним, внутрішня процедура завантаження документа очищає читач, таблицю перехресних посилань та обробник шифрування як частину провалу, точно так, як має бути, а це означає, що не залишається напівпобудованого парсера, що чекав би на виправлений пароль при другому виклику. Проведіть той самий об'єкт через ще одну спробу завантаження все одно, і режим збою точно того типу, що жалюгідно налагоджувати: помилка проявляється з внутрішнього стану, побудованого для іншого, уже провального розбору, з нічим у ній, що очевидно вказувало б назад на пароль трьома викликами вище за течією. PDFlibPas уникає всього цього класу проблем, ніколи не намагаючись відновити об'єкт документа, щойно той провалив відкриття; кожна спроба отримує документ, що ніколи не бачив неправильного пароля, включно з читачем і таблицею перехресних посилань

Як колбек OnPassword запитує наступний пароль?

TPDFlibPasswordEvent — тип колбека, який PDFlibPas викликає через 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, і PDFlibPas пробує знову з тим, що обробник щойно записав у Password

Всередині циклу повторних спроб: новий TPDFDocument для кожної спроби

Внутрішньо PDFlibPas відповідає на питання життєвого циклу об'єкта однаково для LoadFromFile, LoadFromStream та LoadFromString: кожна спроба, включно з першою, конструює свіжий TPDFDocument, проганяє його через повну послідовність відкриття з будь-яким паролем, який ця спроба використовує, і зберігає об'єкт лише якщо пароль верифікується. TPDFDocument відхиленої спроби негайно звільняється, забираючи з собою свій читач, таблицю перехресних посилань та обробник шифрування, і наступна спроба починається заново з об'єктом, що взагалі не має історії

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

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

Скільки разів PDFlibPas повторить спробу з неправильним паролем?

PDFlibPas дозволяє шістнадцять загальних спроб проти одного виклику LoadFromFile, LoadFromStream чи LoadFromString, рахуючи пароль, переданий у сам виклик, як спробу один. OnPassword спрацьовує лише для спроб від двох до шістнадцяти, що обмежує колбек п'ятнадцятьма викликами; запросіть сімнадцяту спробу, і PDFlibPas відмовляє, навіть не викликаючи обробник. Залиште Retry на його значенні за замовчуванням false у будь-якій точці, чи вичерпайте всі шістнадцять спроб без правильного пароля, і LoadFromFile повертає 0 з LastErrorCode, встановленим у 404, кодом PDFlibPas для відхиленого пароля. Обмеження існує з причин, що виходять за межі акуратності: необмежений цикл повторних спроб — легкий спосіб перетворити одну помилково введену пароль на випадкову відмову в обслуговуванні проти будь-якого потоку, що виконує завантаження, особливо коли обробник підключений до чогось автоматизованого, на кшталт списку раніше бачених паролів, а не людини, що клацає діалогом. PDFlibPas також дотримується Abort, викликаного на екземплярі TPDFlib зсередини обробника, оскільки Sender прибуває як той самий об'єкт, корисно за кнопкою Скасувати на діалозі пароля, і зупиняє цикл повторних спроб на наступній перевірці незалежно від того, у що було встановлено Retry. Завантаження, що провалюється з причини, відмінної від неправильного пароля, наприклад пошкодженої таблиці перехресних посилань, узагалі ніколи не входить у цикл повторних спроб: PDFlibPas повідомляє LastErrorCode 401 і зупиняється після першої спроби, бо жодна кількість вгадувань пароля не виправить структурно зламаний файл

Чи цикл повторних спроб працює однаково для файлів, потоків та рядків?

Колбек OnPassword та обмеження шістнадцяти спроб поводяться ідентично через LoadFromFile, LoadFromStream та LoadFromString, хоча ці три точки входу тримають своє джерело по-різному між спробами. Шлях файлу дешево переглянути знову, оскільки кожна спроба просто повторно відкриває названий файл, а джерело-рядок уже сидить у пам'яті як власна копія викликача, тож жодному з них не потрібна жодна допомога від викликача між спробами. Потік, наданий викликачем, — той єдиний випадок, вартий паузи: LoadFromStream перемотує цей потік назад у позицію нуль і копіює його внутрішньо перед першою спробою розбору, тож кожна наступна спроба, і щойно сконструйований TPDFDocument за нею, відтворюється з цієї внутрішньої копії, а не з того місця, де провалений розбір залишив позицію потоку. Передайте PDFlibPas TFileStream чи TMemoryStream для документа, захищеного паролем, і немає потреби перемотувати його між повторними спробами; PDFlibPas уже враховує позицію, яку могла перемістити перша, невдала спроба

Вписування повторної спроби пароля в екран прийому документів

Робочий процес прийому документів — природний дім для цього колбека, бо це саме та форма проблеми, для розв'язання якої побудований OnPassword: файл прибуває ззовні застосунку, його пароль не відомий із певністю заздалегідь, а особі, що надає кандидатів, потрібно більше однієї спроби без того, щоб навколишній код писав власний цикл повторних спроб навколо 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 отримує Lib лише коли LoadFromFile повернув 1, означаючи, що якийсь пароль у цьому обміні справді верифікувався проти обробника шифрування файлу; відхилена спроба ніколи не досягає цього рядка, як і напіввідкритий документ. Що йде далі, щойно документ на кшталт цього підтверджено відкритим, варте другого погляду на його налаштування захисту, а не припущення, що пароль, який спрацював, — уся історія безпеки: аудит того, що фактично оголошує словник /Encrypt документа охоплює читання алгоритму, ревізії та бітів дозволів, які відкриває PDFlibPas, щойно такий файл завантажується

Повторна спроба пароля також — вузький приклад ширшої дисципліни, яку PDFlibPas застосовує по всьому своєму шару розбору: файл, що ще не довів себе, не отримує жодної переваги сумніву, чи питання в тому, який пароль його розблоковує, чи в тому, чи поле довжини всередині нього бреше про розмір буфера, який йому потрібен. Загартування парсера PDF на Pascal проти зловмисних файлів охоплює іншу половину цієї дисципліни, декодери, що трактують кожну програму шрифту та потік зображення у вхідному PDF як ворожий вхід, а не коректно сформований документ, що просто забув свій пароль

OnPassword та цикл повторних спроб за ним — частина стандартної бібліотеки PDF PDFlibPas для Delphi та C++Builder, доступної всюди, де вже доступні LoadFromFile, LoadFromStream чи LoadFromString, без потреби в окремому модулі чи рівні ліцензії для документа, якому просто потрібна друга спроба вгадати свій пароль