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

Отваряне на криптирани 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 point-а са добавени във v3.496.0

Сценарият е тесен, но реален. Forensics engagement ви дава key, възстановен от memory image, и никакъв password. Bulk archival run има десет хиляди document-а, чиито file key-ове стоят в escrow database, защото системата за DRM, от която са дошли, е спряла да издава password-и преди години. Migration от пенсиониран rights-management продукт разполага с 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, hashed в друг nonsense, затова е нужен отделен entry point, а не flag към DAOpenFile

Къде може да се инжектира raw key е фиксирано от revision-а. Revision-и 2 до 4 все още извеждат отделен per-object key от file key, object number, generation number и при AESV2 AES salt-а, така че наличието на file key изобщо не позволява да пропуснете object-level derivation. Revision-и 5 до 7 използват директно 32-byte file key за AES-256, без per-object стъпка. Общият слой и за двата случая е самият file key и точно там PDF Library for Delphi приема външно подаден key. Ако все пак имате password, останете на обикновения path и оставете password retry lifecycle-а да обработи грешния първи опит, защото raw-key entry point-ът умишлено се отказва от няколко удобства, които password path-ът запазва

Какъв input приема DAOpenFileWithEncryptionKey?

Само ASCII hex без prefix и whitespace, с четен брой знаци, като декодираният byte count трябва точно да съвпада с encryption revision-а. За revision-и 2 до 4 очакваната дължина идва от /Length в encryption dictionary: число битове, кратно на 8, между 5 и 16 bytes, с default 40 bits, когато /Length липсва. За revision-и 5 до 7 дължината е точно 32 bytes, без negotiation. Prefix 0x, нечетен брой digits, повече от 64 hex characters, непознат bit в Options или document, който изобщо не е криптиран, водят до един и същ резултат: handle 0 и LastErrorCode, зададен на PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, което е 425. Строгостта е целенасочена. Lenient parser, който trim-ва whitespace и допълва кратък input с нули, с готовност ще превърне truncated 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 code-ове остават различими, така че batch job да може да раздели operator error от проблем с evidence: 411, когато файлът не съществува, 401, когато не може да бъде отворен за read, 409, когато cross-reference structure-ът е повреден. Всичко, свързано с key, се слива в 425 нарочно, защото raw-key entry point, който съобщава коя част от key-а е грешна, би бил oracle

Какво всъщност доказва verification-ът?

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

Когато verifier съществува, но не съвпада, open-ът безусловно се отказва. Това трябва да се каже ясно, защото точно на тази гаранция се крепи цялата feature. Имайте предвид и какво не е verification: той казва, че key-ят decrypt-ва този файл, но не и че някой ви е упълномощил да го използвате. Permission word-ът, възстановен от /Perms, е evidence за key-а, а не разрешение, и ако искате да знаете какво document-ът действително твърди, че позволява, това е отделна задача за encryption и permissions audit pass. Проблеми при password-side normalization, като SASLprep обработката на non-ASCII AES-256 password-и, просто не възникват тук, защото никой string не достига до hash

Кога се прилага PDF_RAW_KEY_ALLOW_UNVERIFIED?

PDF_RAW_KEY_ALLOW_UNVERIFIED покрива точно една ситуация: document-ът не носи usable verifier, защото /Perms липсва или не е 16 bytes, или /U е прекалено кратък за сравнение. Той не може да override-не failing evidence. Повредете един hex digit в /Perms, подайте правилния key с активна option и PDF Library for Delphi пак ще върне 0 и 425. Подайте key от 32 zero bytes срещу непокътнат verifier с активна option и отговорът отново е същият. Option-ът облекчава липсата на proof, никога противоречието с него. Тъй като recovery open и verified open са различни epistemic state-ове, те се докладват отделно, вместо да се скриват в return value, а 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
  // няма verifier в този file? retry под explicit 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;

Read-only по конструкция и кой притежава stream-а

Raw-key file entry-то винаги отваря source-а с fmOpenRead or fmShareDenyWrite и маркира цялата Direct Access chain като read-only, така че DAAppendFile отказва in-place write и връща 2, вместо да опитва incremental update. Това не е policy, от която можете да отговорите handle-а; тя се задава в constructor-а, преди file-ът изобщо да бъде parsed. За evidence работа важната характеристика е, че source bytes са byte-identical след затваряне на handle-а, а regression suite-ът проверява точно това върху AES-128 revision 4 fixture и AES-256 revision 6 fixture. Stream entry-то се държи по същия начин при write-ове и добавя още едно правило: 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: export-вайте другаде, никога не append-вайте към evidence
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // handle-ът никога не е притежавал този stream
End;

Хигиена на key-овете и какво export-ва DLL

Затварянето на chain-а overwrites-ва file key, password cache-а и derived object key-овете, а decoded key-ят се wipe-ва във Finally block-а на самия entry point, независимо дали open-ът е успял. Има една тънкост, която струваше реално debugging време: всяко копие, което трябва да оцелее, се clone-ва изрично с SetLength плюс Move, вместо да се присвоява. Присвоете един AnsiString на друг в Delphi и двете имена споделят един buffer чрез copy-on-write, така че wiping-ът на caller side ще занули key-а, който crypt handler-ът още използва, и document-ът ще се decrypt-не до garbage по причини, които никой stack trace не би обяснил. Само file-based entry point-ите преминават DLL boundary, във wide и ANSI форми, заедно с validation status accessor-а; TStream variant-ът остава само за Delphi, защото зависи от lifetime и reference semantics на Delphi object-и, които нямат честно представяне във flat C ABI. Ако recovery tooling-ът ви е DLL client, планирайте staging във временен файл и изтриването му под същите controls, които прилагате към key-а

Третирайте hex key-а като credential material със същите правила за работа, които бихте дали на document password, и пазете validation status-а във всеки log, който chain of custody произвежда, така че по-късен читател да може да различи verified extraction от unattested. Ако оценявате Delphi PDF component за forensics, bulk archival или DRM migration, raw-key entry point-ите, read-only гаранцията и encryption audit surface-ът са част от една и съща library, а пълният списък с feature-и е на продуктовата страница на PDF Library for Delphi