PDF Library for Delphi умеет открывать зашифрованный PDF по raw-ключу шифрования файла, а не по паролю. DAOpenFileWithEncryptionKey принимает ключ в виде шестнадцатеричного текста, сверяет его с проверочным значением, уже хранящимся в словаре шифрования, и возвращает Direct Access handle только для чтения; DAOpenFromStreamWithEncryptionKey делает то же самое для принадлежащего вызывающему коду TStream. Обе точки входа появились в v3.496.0
Сценарий узкий, но вполне реальный. В рамках forensic-задачи вам передают ключ, восстановленный из образа памяти, но не пароль. Пакетный архив содержит десять тысяч документов, чьи ключи файлов лежат в escrow-базе, потому что исходная DRM-система много лет назад перестала выдавать пароли. При миграции с устаревшего продукта управления правами остаётся материал ключей и ничего больше. Во всех таких случаях имеющийся у вас credential — результат derivation ключа, а не его вход, и ни один параметр пароля в API его не примет
Почему ключ шифрования файла — не пароль?
Пароль и ключ шифрования файла находятся по разные стороны derivation ключа в стандартном обработчике безопасности PDF (ISO 32000-1 §7.6.3). Обработчик берёт пароль, смешивает его с /O, /P, идентификатором файла и хешем, зависящим от revision, и получает ключ файла. Если подать ключ файла в поле пароля, получится бессмыслица, которая затем хешируется в другую бессмыслицу, поэтому для этого сценария нужна отдельная точка входа, а не флаг у DAOpenFile
Место, куда можно подставить raw-ключ, определяется revision. В revision 2–4 по-прежнему выводится отдельный ключ для каждого объекта из ключа файла, номера объекта, номера generation и, для AESV2, соли AES, поэтому наличие ключа файла вообще не позволяет пропустить derivation на уровне объекта. В revision 5–7 32-байтный ключ файла используется непосредственно для AES-256, без шага для каждого объекта. Общим слоем для обоих вариантов остаётся сам ключ файла, поэтому только в этом месте PDF Library for Delphi принимает ключ, переданный извне. Если пароль всё-таки есть, лучше остаться на обычном пути и позволить циклу повторных попыток пароля обработать неверную первую попытку, поскольку raw-key entry point намеренно отказывается от нескольких удобств, которые сохраняет парольный путь
Какие входные данные принимает DAOpenFileWithEncryptionKey?
Только ASCII-hex без префикса, пробелов и с чётным числом символов; число декодированных байт должно точно соответствовать encryption revision. Для revision 2–4 ожидаемая длина берётся из /Length в словаре шифрования: число бит, кратное 8, дающее от 5 до 16 байт, при отсутствии /Length используется значение 40 бит. Для revision 5–7 длина ровно 32 байта, без переговоров. Префикс 0x, нечётное число цифр, более 64 hex-символов, неизвестный бит в Options или документ, который вообще не зашифрован, приводят к одному результату: handle равен 0, а LastErrorCode получает значение PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, то есть 425. Строгость здесь намеренна. Снисходительный парсер, который удаляет пробелы и дополняет короткий ввод нулями, легко превратит обрезанную вставку из буфера обмена в credential, а затем упадёт в месте, куда сложнее добраться по диагностике
Var
Lib: TPDFlib;
FileHandle, PageRef: Integer;
Begin
Lib:= TPDFlib.Create;
Try
// KeyHex содержит 32 hex-символа для AES-128 R4 и 64 для AES-256 R6
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If FileHandle= 0 Then
Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
Finally
Lib.DACloseFile(FileHandle);
End;
Finally
Lib.Free;
End;
End;
Остальные коды отказа остаются различимыми, чтобы пакетная задача могла отделить ошибку оператора от проблем с доказательствами: 411, если файла нет, 401, если его нельзя открыть для чтения, 409, если структура cross-reference повреждена. Всё, что связано с ключом, намеренно сводится к 425, потому что raw-key entry point, сообщающий, какая часть ключа неверна, становится oracle
Что именно доказывает проверка?
PDF Library for Delphi доказывает, что переданный ключ принадлежит этому документу, используя verifier, который уже содержится в словаре шифрования, причём проверка зависит от revision. Revision 2 заново вычисляет RC4-шифрование стандартной строки padding длиной 32 байта и сравнивает все 32 байта с /U. Revision 3 и 4 хешируют padding вместе с ID файла, выполняют проход RC4 и ещё 19 раундов, выведенных через XOR, затем сравнивают первые 16 байт /U. Revision 5–7 расшифровывают 16-байтную строку /Perms с нулевым IV и одновременно проверяют четыре независимых условия: little-endian permission word против /P, четыре байта FF в позициях с 5 по 8, флаг шифрования metadata как T или F и маркер adb в позициях с 10 по 12
Если verifier существует, но не совпадает, открытие безусловно отклоняется. Это стоит сформулировать прямо, потому что на этой гарантии держится вся возможность. Заметьте также, чего проверка не доказывает: она говорит, что ключ расшифровывает этот файл, но не то, что кто-либо разрешил вам его использовать. Permission word, извлечённое из /Perms, свидетельствует о ключе, а не выдаёт разрешение, и если нужно узнать, что документ фактически заявляет разрешённым, это отдельная задача для аудита шифрования и разрешений. Проблемы нормализации на стороне пароля, например обработка SASLprep для не-ASCII паролей AES-256, здесь просто не возникают, поскольку ни одна строка не доходит до хеша
Когда действует PDF_RAW_KEY_ALLOW_UNVERIFIED?
PDF_RAW_KEY_ALLOW_UNVERIFIED покрывает ровно одну ситуацию: документ не содержит пригодного verifier, потому что /Perms отсутствует или имеет длину не 16 байт, либо /U слишком короток для сравнения. Параметр не может отменить провалившееся доказательство. Исправьте один hex-символ внутри /Perms и передайте правильный ключ с установленной опцией — PDF Library for Delphi всё равно вернёт 0 и 425. Передайте ключ из 32 нулевых байт для целого verifier с установленной опцией — результат будет тем же. Опция смягчает отсутствие доказательства, но никогда не отменяет противоречие. Поскольку recovery-open и verified-open — разные epistemic states, они также сообщаются отдельно, а не сворачиваются в return value, и DAGetEncryptionKeyValidation принимает handle открытия и возвращает одну из трёх констант:
PDF_RAW_KEY_VALIDATION_VERIFIED(1) означает, что verifier присутствовал и совпалPDF_RAW_KEY_VALIDATION_UNVERIFIED(2) означает, что ключ принят только потому, что verifier нельзя было оценить, а вызывающий код явно запросил такую policyPDF_RAW_KEY_VALIDATION_NONE(0) возвращается handle, открытому обычным паролем
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
(Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
// В этом файле нет verifier? Повторить с явно заданной recovery policy
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
PDF_RAW_KEY_VALIDATION_VERIFIED:
Chain.Note('key verified against the encryption dictionary');
PDF_RAW_KEY_VALIDATION_UNVERIFIED:
Chain.Note('no verifier available: extraction is unattested');
End;
End;
Только для чтения по конструкции и кто владеет потоком
Файловая точка входа raw-key всегда открывает источник с fmOpenRead or fmShareDenyWrite и делает всю цепочку Direct Access доступной только для чтения, поэтому DAAppendFile отказывается от записи на месте и возвращает 2 вместо попытки инкрементального обновления. Это не policy, от которой handle можно уговорить отказаться: она задаётся в конструкторе ещё до разбора файла. В работе с доказательствами важно, что байты источника после закрытия handle остаются побайтно идентичными, и регрессионный набор ровно это проверяет для fixture revision 4 с AES-128 и для fixture revision 6 с AES-256. Потоковая точка входа так же ведёт себя при записи и добавляет ещё одно правило: DAOpenFromStreamWithEncryptionKey никогда не забирает владение, поэтому DACloseFile оставляет ваш TStream живым, а освобождать его нужно самостоятельно
Source:= TMemoryStream.Create;
Try
Source.LoadFromFile(ArchivePath);
Source.Position:= 0;
FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
If FileHandle<> 0 Then
Try
// 2 = handle только для чтения: экспортировать в другое место, не добавлять к доказательствам
Assert(Lib.DAAppendFile(FileHandle)= 2);
Harvest(Lib, FileHandle);
Finally
Lib.DACloseFile(FileHandle);
End;
Finally
Source.Free; // handle никогда не владел этим потоком
End;
Гигиена ключа и что экспортирует DLL
Закрытие цепочки затирает ключ файла, кэш пароля и производные ключи объектов, а декодированный ключ очищается в блоке Finally самой точки входа независимо от того, успешно ли прошло открытие. За этим стоит тонкость, которая отняла немало времени при отладке: любую копию, которую нужно сохранить, клонируют явно через SetLength и Move, а не присваивают. Если присвоить один AnsiString другому в Delphi, оба имени при copy-on-write разделяют один буфер, поэтому очистка со стороны вызывающего кода обнулит ключ, которым всё ещё пользуется crypt handler, и документ расшифруется в мусор по причинам, которые не объяснит ни один stack trace. Через границу DLL проходят только файловые точки входа, в wide- и ANSI-формах, вместе с accessor статуса валидации; вариант TStream остаётся только для Delphi, потому что зависит от времени жизни объектов Delphi и семантики ссылок, которые невозможно честно представить в плоском C ABI. Если ваш recovery-инструмент — DLL-клиент, планируйте промежуточную запись во временный файл и его удаление под теми же контролями, что применяются к ключу
Считайте hex-ключ credential-материалом и обращайтесь с ним по правилам, которые применили бы к паролю документа, а статус валидации сохраняйте в журнале, который формирует ваша цепочка хранения доказательств, чтобы последующий читатель мог отличить проверенное извлечение от извлечения без аттестации. Если вы выбираете PDF-компонент для Delphi для forensic-задач, пакетного архивирования или миграции DRM, точки входа raw-key, гарантия read-only и поверхность аудита шифрования входят в одну и ту же библиотеку, а полный список возможностей приведён на странице продукта PDF Library for Delphi