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

Відкриття зашифрованих PDF за raw file key у Delphi

PDF Library for Delphi може відкрити зашифрований PDF за його raw file encryption key замість password. DAOpenFileWithEncryptionKey приймає key як hexadecimal text, перевіряє його за verifier, уже збереженим в encryption dictionary, і повертає read-only Direct Access handle; DAOpenFromStreamWithEncryptionKey робить те саме для TStream, яким володіє caller. Обидва entry points з’явилися у v3.496.0

Сценарій вузький, але цілком реальний. Під час forensic engagement вам передають key, відновлений із memory image, але без password. Bulk archival run має десять тисяч document-ів, чиї file keys лежать у escrow database, бо початкова DRM system перестала видавати passwords багато років тому. Під час migration із retired rights-management product залишилися key material і більше нічого. У кожному з цих випадків credential у вас — результат key derivation, а не вхідні дані, і жоден password parameter в API його не прийме

Чому file encryption key — це не password?

Password і file encryption key перебувають по різні боки key derivation у PDF standard security handler (ISO 32000-1 §7.6.3). Handler бере password, змішує його з /O, /P, file ID і hash, специфічним для revision, та отримує file key. Якщо передати file key у password slot, ви отримаєте nonsense, хешований у інший nonsense, тому потрібен окремий entry point, а не прапорець для DAOpenFile

Місце, куди можна підставити raw key, фіксоване revision. Revisions 2–4 усе ще виводять окремий per-object key із file key, object number, generation number і, для AESV2, AES salt, тому наявність file key взагалі не дозволяє пропустити object-level derivation. Revisions 5–7 безпосередньо використовують 32-byte file key для AES-256, без per-object step. Спільний для обох рівень — сам file key, і саме в цю точку PDF Library for Delphi приймає зовнішній key. Якщо password у вас усе ж є, залишайтеся на звичайному шляху й дозвольте password retry lifecycle обробити неправильну першу спробу, бо raw-key entry навмисно відмовляється від кількох зручностей, які зберігає password path

Який input приймає DAOpenFileWithEncryptionKey?

Лише ASCII hex без префікса й пробілів, парної довжини, причому кількість decoded bytes має точно відповідати encryption revision. Для revisions 2–4 очікувана довжина береться з /Length в encryption dictionary: це кратне 8 bits значення від 5 до 16 bytes, а коли /Length відсутній, використовується 40 bits. Для revisions 5–7 це рівно 32 bytes, без negotiation. Префікс 0x, непарна кількість digits, понад 64 hex characters, невідомий bit у Options або document, який взагалі не зашифрований, дають однаковий результат: handle 0 і LastErrorCode зі значенням PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, тобто 425. Суворість тут навмисна. Поблажливий parser, який обрізає whitespace і доповнює короткий input нулями, легко перетворить обрізану clipboard paste на credential, а потім провалиться значно менш зрозуміло

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex має 32 hex characters для 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;

Інші failure codes залишаються різними, щоб batch job міг відрізнити operator error від проблеми з evidence: 411, коли file не існує, 401, коли його не можна відкрити для читання, 409, коли cross-reference structure пошкоджена. Усе, що стосується key, зводиться до 425 навмисно, бо raw-key entry point, який повідомляє, яка частина key була неправильною, стає oracle

Що насправді доводить verification?

PDF Library for Delphi доводить, що переданий key належить цьому document, використовуючи verifier, який уже міститься в encryption dictionary, причому check відрізняється для різних revisions. Revision 2 повторно обчислює RC4 encryption 32-byte standard padding string і порівнює всі 32 bytes з /U. Revisions 3 і 4 хешують padding разом із file ID, виконують RC4 pass плюс 19 XOR-derived rounds і порівнюють перші 16 bytes /U. Revisions 5–7 розшифровують 16-byte /Perms string із zero IV і водночас перевіряють чотири незалежні речі: little-endian permission word проти /P, чотири bytes FF на позиціях 5–8, encrypt-metadata flag як T або F та marker adb на позиціях 10–12

Коли verifier існує, але не збігається, open безумовно відхиляється. Це варто сказати прямо, бо на цьому тримається гарантія всієї feature. Також важливо розуміти, чим verification не є: вона каже, що key розшифровує цей file, а не що хтось уповноважив вас його використовувати. Permission word, відновлений із /Perms, є свідченням про key, а не grant, і якщо потрібно знати, що саме document заявляє як дозволене, це окрема робота для encryption and permissions audit pass. Проблеми password-side normalization, як-от SASLprep для non-ASCII AES-256 passwords, тут просто не виникають, бо жоден string не доходить до hash

Коли застосовується PDF_RAW_KEY_ALLOW_UNVERIFIED?

PDF_RAW_KEY_ALLOW_UNVERIFIED охоплює рівно одну ситуацію: document не має usable verifier, бо /Perms відсутній або має не 16 bytes, або /U надто короткий для порівняння. Він не може скасувати failing evidence. Зіпсуйте одну hex digit усередині /Perms і передайте правильний key із цим option — PDF Library for Delphi усе одно поверне 0 і 425. Передайте key із 32 zero bytes проти непошкодженого verifier з цим option — результат той самий. Option послаблює відсутність proof, але ніколи не скасовує суперечливе proof. Оскільки recovery open і verified open — різні epistemic states, вони також звітуються окремо, а DAGetEncryptionKeyValidation приймає open handle і повертає одну з трьох constants:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) означає, що verifier існував і збігся
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) означає, що key прийнято лише тому, що verifier не можна було оцінити, а caller явно попросив цю policy
  • PDF_RAW_KEY_VALIDATION_NONE (0) повертає handle, відкритий звичайним password
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // у цьому file немає 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;

Лише для читання за конструкцією: хто володіє stream

File entry для raw key завжди відкриває source з fmOpenRead or fmShareDenyWrite і позначає весь Direct Access chain як read-only, тому DAAppendFile відмовляється від in-place writing і повертає 2 замість спроби incremental update. Це не policy, яку можна вмовити змінити через handle; вона встановлюється в constructor ще до parsing file. Для роботи з evidence потрібна властивість, що source bytes залишаються byte-identical після закриття handle, і regression suite саме це перевіряє на fixture AES-128 revision 4 та AES-256 revision 6. Stream entry поводиться так само щодо writes і додає ще одне правило: DAOpenFromStreamWithEncryptionKey ніколи не забирає ownership, тому DACloseFile залишає ваш TStream живим, а звільняєте його ви самі

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = read-only handle: експортувати в інше місце, ніколи не дописувати evidence
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // handle ніколи не володів цим stream
End;

Гігієна key і що експортує DLL

Закриття chain перезаписує file key, password cache і derived object keys, а decoded key стирається у блоці Finally самого entry point, незалежно від того, чи був open успішним. За цим стоїть деталь, яка забрала чимало часу на debugging: будь-яка копія, що має пережити операцію, явно клонуються через SetLength плюс Move, а не присвоюється. Якщо в Delphi присвоїти один AnsiString іншому, обидва імені поділять один buffer за copy-on-write, тому стирання на стороні caller обнулить key, яким crypt handler ще користується, і document розшифрується в garbage з причин, яких не пояснить жоден stack trace. Через DLL boundary проходять лише file-based entry points у wide та ANSI forms, разом із validation status accessor; варіант TStream залишається Delphi-only, бо залежить від lifetime Delphi objects і reference semantics, для яких немає чесного представлення у flat C ABI. Якщо ваш recovery tooling є DLL client, плануйте staging у temporary file та його видалення під тими самими controls, що й для key

Ставтеся до hex key як до credential material і поводьтеся з ним за правилами, які дали б document password, а validation status зберігайте в тому log, який формує ваша chain of custody, щоб пізніший читач міг відрізнити verified extraction від unattested one. Якщо ви оцінюєте Delphi PDF component для forensics, bulk archival або DRM migration, raw-key entry points, read-only guarantee та encryption audit surface є частинами тієї самої library, а повний список feature можна переглянути на сторінці продукту PDF Library for Delphi