مقاله فنی

باز کردن PDF رمزگذاری‌شده با raw file key در Delphi

PDF Library for Delphi می‌تواند یک PDF رمزگذاری‌شده را به‌جای password، با raw file encryption key آن باز کند. DAOpenFileWithEncryptionKey کلید را به‌صورت متن hexadecimal می‌پذیرد، آن را با verifier ذخیره‌شده در encryption dictionary بررسی می‌کند و یک Direct Access handle فقط‌خواندنی برمی‌گرداند؛ DAOpenFromStreamWithEncryptionKey همین کار را برای یک TStream تحت مالکیت caller انجام می‌دهد. هر دو در v3.496.0 اضافه شدند

این سناریو محدود است اما واقعی است. یک کار forensic کلیدی را که از memory image بازیابی شده به شما می‌دهد و passwordی در کار نیست. یک اجرای bulk archival ده هزار document دارد که file keyهایشان در escrow database قرار گرفته‌اند، چون DRM system مبدأ سال‌هاست password صادر نمی‌کند. در مهاجرت از یک 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 وارد کنید، نتیجه بی‌معنی دیگری hash می‌شود و بی‌معنی دیگری تولید می‌کند؛ به همین دلیل این قابلیت entry point خودش را لازم دارد، نه flagی روی DAOpenFile

محل تزریق raw key بر اساس revision ثابت است. revisionهای 2 تا 4 همچنان یک object key جداگانه را از file key، object number، generation number و برای AESV2 از AES salt می‌سازند، بنابراین داشتن file key اصلاً اجازه نمی‌دهد مرحله object-level derivation را حذف کنید. revisionهای 5 تا 7 برای AES-256 مستقیماً از file key 32 بایتی استفاده می‌کنند و مرحله per-object ندارند. لایه مشترک هر دو، خود file key است و PDF Library for Delphi فقط در همین نقطه کلید خارجی را می‌پذیرد. اگر هنوز password را دارید، به مسیر معمول برگردید و بگذارید چرخه retry password، اولین تلاش اشتباه را مدیریت کند، چون entry point مربوط به raw key عمداً چند convenience را که مسیر password حفظ می‌کند کنار می‌گذارد

DAOpenFileWithEncryptionKey چه inputی را می‌پذیرد؟

فقط hex ASCII بدون prefix و بدون whitespace، با طول زوج؛ تعداد byteهای decode‌شده نیز باید دقیقاً با revision رمزگذاری برابر باشد. برای revisionهای 2 تا 4، طول مورد انتظار از /Length در encryption dictionary می‌آید: مضربی از 8 bit که بین 5 و 16 byte قرار می‌گیرد و اگر /Length وجود نداشته باشد، 40 bit در نظر گرفته می‌شود. برای revisionهای 5 تا 7 دقیقاً 32 byte لازم است و مذاکره‌ای وجود ندارد. prefix 0x، تعداد رقم فرد، بیش از 64 کاراکتر hex، bit ناشناخته در Options یا documentی که اصلاً encrypted نیست، همگی یک نتیجه دارند: handle برابر 0 و LastErrorCode برابر PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID که مقدارش 425 است. این سخت‌گیری عمدی است. parser سهل‌گیر که whitespace را trim کند و input کوتاه را با zero پر کند، با خیال راحت یک paste ناقص clipboard را credential می‌سازد و بعد در نقطه‌ای بسیار نامفهوم‌تر failure می‌دهد

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex برای AES-128 R4 برابر 32 کاراکتر hex و برای AES-256 R6 برابر 64 کاراکتر است
    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 دیگر متمایز باقی می‌مانند تا یک batch job بتواند خطای operator را از مشکل evidence جدا کند: 411 وقتی file وجود ندارد، 401 وقتی برای خواندن باز نمی‌شود و 409 وقتی ساختار cross-reference خراب است. همه مسائل مربوط به key عمداً در 425 جمع می‌شوند، چون entry point مربوط به raw key اگر گزارش دهد کدام بخش key اشتباه بوده، به یک oracle تبدیل می‌شود

verification واقعاً چه چیزی را ثابت می‌کند؟

PDF Library for Delphi ثابت می‌کند key ارائه‌شده به همین document تعلق دارد؛ برای این کار از verifier موجود در encryption dictionary استفاده می‌کند و check بر اساس revision فرق دارد. revision 2 رمزگذاری RC4 مربوط به string استاندارد padding با طول 32 byte را دوباره محاسبه می‌کند و هر 32 byte را با /U مقایسه می‌کند. revisionهای 3 و 4 padding را همراه file ID hash می‌کنند، pass مربوط به RC4 و 19 round مشتق‌شده از XOR را اجرا می‌کنند و 16 byte اول /U را می‌سنجند. revisionهای 5 تا 7 string شانزده بایتی /Perms را با IV صفر decrypt می‌کنند و چهار مورد مستقل را هم‌زمان check می‌کنند: permission word به‌صورت little-endian در برابر /P، چهار byte از نوع FF در positionهای 5 تا 8، flag مربوط به encrypt metadata به‌شکل T یا F و marker adb در positionهای 10 تا 12

اگر verifier وجود داشته باشد اما match نشود، open بدون استثنا رد می‌شود. این نکته باید روشن گفته شود، چون guarantee کل feature بر آن استوار است. همچنین verification این نیست که به شما اجازه استفاده از document داده شده باشد؛ فقط می‌گوید key این file را decrypt می‌کند. permission word بازیابی‌شده از /Perms درباره key evidence است، نه grant، و اگر می‌خواهید بدانید document واقعاً چه چیزی را مجاز اعلام می‌کند، این کار جداگانه‌ای برای audit رمزگذاری و permissionها است. مشکلات normalization در سمت password، مانند مدیریت SASLprep برای passwordهای AES-256 غیر ASCII، در اینجا اصلاً رخ نمی‌دهند، چون هیچ stringی به hash نمی‌رسد

PDF_RAW_KEY_ALLOW_UNVERIFIED چه زمانی کاربرد دارد؟

PDF_RAW_KEY_ALLOW_UNVERIFIED دقیقاً یک وضعیت را پوشش می‌دهد: document هیچ verifier قابل‌استفاده‌ای ندارد، چون /Perms غایب است یا 16 byte نیست، یا /U برای مقایسه بیش از حد کوتاه است. این گزینه نمی‌تواند evidence ناموفق را override کند. یک رقم hex داخل /Perms را خراب کنید و با فعال بودن option، key درست را بدهید؛ PDF Library for Delphi همچنان 0 و 425 برمی‌گرداند. یک key شامل 32 byte صفر را در برابر verifier سالم، با همان option، امتحان کنید؛ نتیجه همان است. option فقط نبود proof را relax می‌کند، نه تناقض با proof را. چون recovery open و verified open دو وضعیت epistemic متفاوت هستند، جداگانه report می‌شوند و در return value با هم ادغام نمی‌شوند؛ DAGetEncryptionKeyValidation handle بازشده را می‌گیرد و یکی از سه constant زیر را پاسخ می‌دهد:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) یعنی verifier وجود داشته و match شده است
  • 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 وجود ندارد؟ با policy صریح recovery دوباره امتحان کن
  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;

فقط‌خواندنی از روی construction و مالک stream چه کسی است؟

entry مربوط به raw-key file همیشه source را با fmOpenRead or fmShareDenyWrite باز می‌کند و کل Direct Access chain را read-only علامت می‌زند؛ بنابراین DAAppendFile نوشتن in-place را رد می‌کند و به‌جای تلاش برای incremental update مقدار 2 برمی‌گرداند. این policy چیزی نیست که بتوان handle را از آن منصرف کرد؛ پیش از parse شدن file در constructor تنظیم شده است. در کار evidence، property مورد نظر این است که byteهای source بعد از بسته شدن handle byte-identical باقی بمانند و regression suite دقیقاً همین را روی یک fixture از AES-128 revision 4 و یک fixture از AES-256 revision 6 assert می‌کند. entry مربوط به stream نیز در write همین رفتار را دارد و یک rule دیگر اضافه می‌کند: 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 = handle فقط‌خواندنی: در جای دیگری export کن و هرگز به evidence اضافه نکن
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // handle هرگز مالک این stream نبوده است
End;

بهداشت key و چیزهایی که DLL export می‌کند

بستن chain، file key، password cache و object keyهای مشتق‌شده را overwrite می‌کند و key decode‌شده در block Finally خود entry point wipe می‌شود، چه open موفق شده باشد چه نه. نکته ظریفی پشت این رفتار وجود دارد که زمان واقعی برای debugging گرفت: هر copyای که باید زنده بماند به‌صراحت با SetLength به‌اضافه Move clone می‌شود، نه با assignment. اگر در Delphi یک AnsiString را به دیگری assign کنید، هر دو نام تحت copy-on-write یک buffer را share می‌کنند؛ پس wipe کردن سمت caller، key مورد استفاده crypt handler را صفر می‌کند و document به دلایلی که هیچ stack traceای توضیح نمی‌دهد به garbage decrypt می‌شود. فقط entry pointهای مبتنی بر file، در شکل‌های wide و ANSI، همراه با accessor وضعیت validation از مرز DLL عبور می‌کنند؛ variant مربوط به TStream فقط Delphi است، چون به lifetime و reference semantics مربوط به objectهای Delphi وابسته است و در یک C ABI تخت نمایش صادقانه‌ای ندارد. اگر recovery tooling شما یک DLL client است، باید staging در یک temporary file و حذف آن را با همان کنترل‌هایی انجام دهید که برای key به کار می‌برید

با hex key مانند credential material رفتار کنید و همان قواعدی را اعمال کنید که برای password یک document به کار می‌بردید. وضعیت validation را در log مربوط به chain of custody نگه دارید تا خواننده بعدی بتواند extraction تأییدشده را از extraction بدون attestation تشخیص دهد. اگر در حال ارزیابی یک Delphi PDF component برای forensics، bulk archival یا مهاجرت DRM هستید، entry pointهای raw-key، guarantee فقط‌خواندنی و سطح audit رمزگذاری، همگی بخشی از یک library واحد هستند و فهرست کامل featureها در صفحه محصول PDF Library for Delphi قرار دارد