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, без необходимости в отдельном модуле или уровне лицензии для документа, которому просто нужна ещё одна попытка угадать свой пароль