مقال تقني

فتح PDF مشفّر بمفتاح الملف الخام في Delphi

تستطيع PDF Library for Delphi فتح ملف PDF مشفّر باستخدام مفتاح تشفير الملف الخام بدلًا من كلمة مرور. تقبل DAOpenFileWithEncryptionKey المفتاح كنص سداسي عشري، وتتحقق منه مقابل أداة التحقق المخزنة أصلًا في قاموس التشفير، ثم تعيد مقبض Direct Access للقراءة فقط؛ وتفعل DAOpenFromStreamWithEncryptionKey الشيء نفسه مع TStream يملكه المستدعي. أضيفت النقطتان في الإصدار v3.496.0

السيناريو ضيق لكنه حقيقي. فقد تسلّمك مهمة أدلة جنائية مفتاحًا مستعادًا من صورة ذاكرة من دون كلمة مرور. وقد تحتوي عملية أرشفة جماعية على عشرة آلاف مستند توجد مفاتيح ملفاتها في قاعدة بيانات escrow لأن نظام DRM الأصلي توقف عن إصدار كلمات المرور منذ سنوات. وقد تتضمن عملية ترحيل من منتج متقاعد لإدارة الحقوق مادة المفاتيح ولا شيء آخر. وفي كل حالة من هذه الحالات تكون بيانات الاعتماد التي تملكها ناتج اشتقاق المفتاح لا مدخله، ولن يقبلها أي معامل كلمة مرور في API

لماذا لا يكون مفتاح تشفير الملف كلمة مرور؟

تقع كلمة المرور ومفتاح تشفير الملف على جانبين متعاكسين من اشتقاق المفتاح في معالج أمان PDF القياسي (ISO 32000-1 §7.6.3). يأخذ المعالج كلمة المرور ويمزجها مع /O و/P ومعرّف الملف وتجزئة تعتمد على المراجعة، ثم ينتج مفتاح الملف. وإذا أدخلت مفتاح الملف في خانة كلمة المرور، تحصل على قيمة لا معنى لها تُجزّأ إلى قيمة أخرى لا معنى لها، ولذلك يحتاج هذا السيناريو إلى نقطة دخول خاصة به بدل علم على DAOpenFile

يتحدد موضع حقن المفتاح الخام بحسب المراجعة. فما زالت المراجعات من 2 إلى 4 تشتق مفتاحًا مختلفًا لكل كائن من مفتاح الملف ورقم الكائن ورقم الجيل، ومن ملح AES في حالة AESV2، ولذلك لا يتيح لك امتلاك مفتاح الملف تجاوز الاشتقاق على مستوى الكائن إطلاقًا. أما المراجعات من 5 إلى 7 فتستخدم مفتاح الملف ذي 32 بايت مباشرة مع AES-256، من دون خطوة لكل كائن. والطبقة المشتركة بين الحالتين هي مفتاح الملف نفسه، ولذلك فهذا هو الموضع الوحيد الذي تقبل فيه PDF Library for Delphi مفتاحًا مقدمًا خارجيًا. وإذا كنت تملك كلمة المرور فعلًا، فابق على المسار العادي ودع دورة إعادة محاولة كلمة مرور PDF المشفّر تتعامل مع المحاولة الأولى الخاطئة، لأن نقطة الدخول بالمفتاح الخام تتخلى عمدًا عن وسائل راحة عدة يحافظ عليها مسار كلمة المرور

ما الإدخال الذي تقبله DAOpenFileWithEncryptionKey؟

تقبل فقط محارف ASCII سداسية عشرية غير مسبوقة ولا تحتوي مسافات وبعدد زوجي، ويجب أن يطابق عدد البايتات المفكوكة المراجعة المطلوبة للتشفير تمامًا. وفي المراجعات من 2 إلى 4 يأتي الطول المتوقع من /Length في قاموس التشفير: عدد من بتات يساوي مضاعفًا لـ8 ويقع بين 5 و16 بايت، مع افتراض 40 بت عندما يغيب /Length. أما المراجعات من 5 إلى 7 فطولها 32 بايت بالضبط من دون تفاوض. وتؤدي بادئة 0x، أو عدد محارف فردي، أو أكثر من 64 محرفًا سداسيًا، أو بت مجهول في Options، أو مستند غير مشفّر أصلًا، كلها إلى النتيجة نفسها: مقبض قيمته 0 وضبط LastErrorCode إلى PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID، وقيمته 425. وهذه الصرامة مقصودة، لأن محللًا متساهلًا يزيل المسافات ويملأ الإدخال القصير بأصفار قد يحوّل بسهولة لصقًا مبتورًا من الحافظة إلى بيانات اعتماد، ثم يفشل في موضع أقل وضوحًا بكثير

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex هو 32 محرفًا سداسيًا لـ 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 عندما يكون هيكل الإسناد الترافقي معطوبًا. أما كل ما يتعلق بالمفتاح فيندمج في 425 عمدًا، لأن نقطة دخول المفتاح الخام التي تبلغ أي جزء من المفتاح كان خاطئًا تتحول إلى oracle

ماذا يثبت التحقق فعلًا؟

تثبت PDF Library for Delphi أن المفتاح المقدم يخص هذا المستند، باستخدام أداة التحقق التي يحملها قاموس التشفير أصلًا، ويختلف الفحص باختلاف المراجعة. تعيد المراجعة 2 حساب تشفير RC4 لسلسلة الحشو القياسية ذات 32 بايتًا وتقارن البايتات الـ32 كلها مع /U. وتحسب المراجعتان 3 و4 تجزئة الحشو مع معرّف الملف، وتشغلان تمريرة RC4 إضافة إلى الجولات الـ19 المشتقة بـXOR، ثم تقارنان أول 16 بايتًا من /U. أما المراجعات من 5 إلى 7 فتفك تشفير سلسلة /Perms ذات 16 بايتًا باستخدام IV صفري، وتتحقق من أربعة أمور مستقلة دفعة واحدة: كلمة الصلاحيات بترتيب little-endian مقابل /P، وبايتات FF الأربعة في المواضع من 5 إلى 8، وعلم تشفير البيانات الوصفية كـT أو F، وعلامة adb في المواضع من 10 إلى 12

عندما توجد أداة تحقق لكنها لا تتطابق، يُرفض الفتح بلا استثناء. وهذه نقطة تستحق الذكر بوضوح، لأن عليها يقوم ضمان الميزة كله. لاحظ أيضًا ما لا يثبته التحقق: فهو يقول إن المفتاح يفك تشفير هذا الملف، لا إن أحدًا منحك صلاحية استخدامه. فكلمة الصلاحيات المستخرجة من /Perms دليل على المفتاح وليست تفويضًا، وإذا أردت معرفة ما يدعي المستند السماح به فعلًا فهذه مهمة منفصلة لـعملية تدقيق التشفير والصلاحيات. أما مشكلات التطبيع من جانب كلمة المرور، مثل معالجة SASLprep لكلمات مرور AES-256 غير ASCII، فلا تظهر هنا ببساطة، إذ لا تصل أي سلسلة إلى تجزئة

متى ينطبق PDF_RAW_KEY_ALLOW_UNVERIFIED؟

يغطي PDF_RAW_KEY_ALLOW_UNVERIFIED حالة واحدة بالضبط: أن لا يحمل المستند أداة تحقق صالحة، لأن /Perms مفقود أو ليس بطول 16 بايتًا، أو لأن /U أقصر من أن تجري مقارنته. ولا يستطيع هذا الخيار تجاوز دليل فاشل. فإذا أفسدت محرفًا سداسيًا واحدًا داخل /Perms وقدمت المفتاح الصحيح مع ضبط الخيار، فستعيد PDF Library for Delphi القيمة 0 و425 رغم ذلك. وإذا مررت مفتاحًا مكوّنًا من 32 بايتًا صفريًا مقابل أداة تحقق سليمة مع ضبط الخيار، فستكون الإجابة نفسها. يخفف الخيار غياب الدليل، ولا يتجاوز أبدًا تناقضه. وبما أن الفتح الاستردادي والفتح المتحقق حالتان معرفيتان مختلفتان، فهما يبلغان منفصلين بدل دمجهما في قيمة الإرجاع، كما يأخذ DAGetEncryptionKeyValidation مقبض الفتح ويجيب بإحدى الثوابت الثلاث:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) تعني وجود أداة تحقق وتطابقها
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) تعني قبول المفتاح فقط لأن تعذر تقييم أداة تحقق، ولأن المستدعي طلب هذه السياسة صراحة
  • PDF_RAW_KEY_VALIDATION_NONE (0) هي ما يعيده مقبض فُتح بكلمة مرور عادية
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // لا توجد أداة تحقق في هذا الملف؟ أعد المحاولة بسياسة استرداد صريحة
  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;

للقراءة فقط بحكم البناء، ومن يملك التدفق؟

تفتح نقطة إدخال الملف بالمفتاح الخام المصدر دائمًا باستخدام fmOpenRead or fmShareDenyWrite وتضع سلسلة Direct Access كلها في وضع القراءة فقط، ولذلك يرفض DAAppendFile الكتابة في مكانها ويعيد 2 بدل محاولة تحديث تزايدي. وليست هذه سياسة يمكن إقناع المقبض بالتخلي عنها، إذ تُضبط في الباني قبل حتى تحليل الملف. وفي عمل الأدلة، الخاصية التي تهمك هي أن بايتات المصدر تبقى مطابقة للبايتات الأصلية بعد إغلاق المقبض، وتثبت مجموعة التراجع ذلك بالضبط على fixture من المراجعة 4 مع AES-128 وعلى آخر من المراجعة 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 = مقبض للقراءة فقط: صدّر إلى مكان آخر ولا تلحق شيئًا بالدليل
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // المقبض لم يملك هذا التدفق قط
End;

نظافة المفتاح وما الذي تصدّره DLL

يكتب إغلاق السلسلة فوق مفتاح الملف وذاكرة التخزين المؤقت لكلمة المرور ومفاتيح الكائنات المشتقة، كما يُمحى المفتاح المفكوك في كتلة Finally داخل نقطة الإدخال نفسها سواء نجح الفتح أم لا. وهناك تفصيل وراء ذلك استهلك وقتًا حقيقيًا في التصحيح: إذ تُستنسخ أي نسخة يلزم أن تبقى صراحة باستخدام SetLength مع Move بدل الإسناد. فإذا أسندت AnsiString إلى أخرى في Delphi، اشترك الاسمان في مخزن واحد بسبب النسخ عند الكتابة، ولذلك سيؤدي محو جانب المستدعي إلى تصفير المفتاح الذي لا يزال معالج التشفير يستخدمه، وسيفك المستند إلى بيانات تالفة لأسباب لا يفسرها أي أثر مكدس. ولا تعبر حدود DLL إلا نقاط الدخول المعتمدة على الملفات، بصيغتي wide وANSI، مع موصل حالة التحقق؛ أما صيغة TStream فتبقى خاصة بـDelphi لأنها تعتمد على عمر كائنات Delphi ودلالات المرجع التي لا تمثلها أمانةً واجهة C ABI مسطحة. وإذا كانت أدوات الاسترداد لديك عميل DLL، فخطط لإيداع الملف في ملف مؤقت وحذفه وفق الضوابط نفسها التي تطبقها على المفتاح

عامل المفتاح السداسي العشري بوصفه مادة اعتماد وبقواعد التعامل التي تمنحها لكلمة مرور مستند، واحتفظ بحالة التحقق في السجل الذي تنتجه سلسلة حفظ الأدلة، حتى يستطيع قارئ لاحق التمييز بين استخراج تم التحقق منه وآخر غير موثّق. وإذا كنت تقيم مكوّن PDF لـDelphi للأدلة الجنائية أو الأرشفة الجماعية أو ترحيل DRM، فإن نقاط الدخول بالمفتاح الخام وضمان القراءة فقط وواجهة تدقيق التشفير كلها جزء من المكتبة نفسها، ويمكنك قراءة قائمة الميزات الكاملة في صفحة منتج PDF Library for Delphi