مقال تقني

تدقيق تشفير PDF والأذونات في Delphi باستخدام PDFlibPas

في أحد خطوط استقبال المستندات، عُزلت دفعة كاملة من ملفات قانونية بوصفها "مشفرة" مع أن كل ملف كان يفتح في Adobe Acrobat من دون طلب كلمة مرور. كان الفحص قد اختزل حالة الأمان إلى قيمة منطقية واحدة. حملت الملفات قاموس /Encrypt فعلا — لكن crypt filters فيها كانت مضبوطة على Identity، أي إن السلاسل والدفوقات مخزنة كنص صريح. مشفرة شكليا، مفتوحة عمليا، وموقوفة يومين بسبب قراءة خاطئة لعلم واحد. تلك الحادثة تعريف جيد لما يجب أن يكون عليه تدقيق التشفير الحقيقي: ليس "هل هو مشفر"، بل أي خوارزمية، وأي مراجعة، وأي كلمات مرور، وأي أذونات، وأي أجزاء من الملف يطالها التشفير فعلا. يعرّض PDFlibPas، محرك PDF من losLab لـ Delphi و C++Builder، كل تلك الإجابات عبر API مسطحة وطبقة فئات typed.

ما الذي يسجله قاموس /Encrypt فعلا

يعرّف ISO 32000-1 §7.6 أمان المستند عبر مجموعة صغيرة من مدخلات القاموس، ويعكسها PDFlibPas واحدا لواحد في سجل TPDFEncryption: إصدار المرشح V والمراجعة R اللذان يختاران عائلة الخوارزمية، و Length لحجم المفتاح، وبتات الأذونات في P، وسلاسل التحقق من كلمة مرور المالك والمستخدم O و U (إضافة إلى OE/UE في AES-256)، وعلم EncryptMetadata، وأسماء crypt filters المطبقة على السلاسل والدفوقات والملفات المضمنة.

هذا السجل صديق المدقق تحديدا لأنه لا يؤول. حادثة الاستقبال أعلاه تظهر في حقول مثل StringFilterIdentity و StreamFilterIdentity: عندما تكون صحيحة، لا تتحول البيانات المقابلة إطلاقا، مهما ادعت حالة التشفير في المستند. وبالمثل يخبرك EncryptMetadata = False أن بيانات XMP الوصفية قابلة للقراءة من أي مفهرس حتى لو لم يكن محتوى الصفحات كذلك — وهذا مهم عندما تعتمد قواعد التوجيه على حقول العنوان أو المؤلف.

فحص أمني بعشر أسطر عبر API المسطحة

في معظم خطوط المعالجة، تجيب أربع استدعاءات مسطحة عن الأسئلة اليومية. تعيد LoadFromFile القيمة 1 عند النجاح؛ وبعدها تعمل فاحصات مستوى المستند على الحالة بعد فك التشفير:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', UserPassword) <> 1 then
      raise Exception.Create('Open failed: wrong password or damaged file');
    Writeln('status    : ', PDF.EncryptionStatus);     // decrypted / encrypted / unknown
    Writeln('algorithm : ', PDF.EncryptionAlgorithm);  // RC4 vs AES family
    Writeln('strength  : ', PDF.EncryptionStrength);   // key length class
    Writeln('owner pw? : ', PDF.CheckPassword(CandidatePassword));
  finally
    PDF.Free;
  end;
end;

تبدو CheckPassword أهم مما يظهر. يميز PDF بين كلمة مرور المستخدم (المطلوبة للفتح) وكلمة مرور المالك (التي تمنح الحقوق الكاملة وتتجاوز الأذونات)، والملف المفتوح بكلمة مرور المستخدم يتصرف بشكل مختلف جدا عن الملف المفتوح بكلمة مرور المالك — البايتات نفسها، لكن الحقوق مختلفة. تجعل طبقة الفئات الفرق صريحا: TPDFDocument.HasUserPassword و HasOwnerPassword يبلغان عمّا هو مضبوط، بينما IsUserPassword و IsOwnerPassword يبلغان أي كلمة فتحت الجلسة الحالية فعلا. يجب أن يسجل سجل التدقيق هذا الفرق، لا قيم كلمات المرور نفسها أبدا.

سلم Strength: من RC4 ذي 40 بت إلى AES-256 revision 6

تأخذ دالتا Encrypt و EncryptFile المسطحتان عددا صحيحا باسم Strength له خمس قيم ذات معنى: 0 لـ RC4 ذي 40 بت، و1 لـ RC4 ذي 128 بت، و2 لـ AES ذي 128 بت (قابل للقراءة منذ Acrobat 7)، و3 لـ AES ذي 256 بت كما قُدم مع Acrobat 9، و4 لـ AES ذي 256 بت كما يتطلب Acrobat X وما بعده.

تستحق القيمتان 3 و4 نظرة أقرب، لأن "AES-256" مخططان مختلفان. تقابل Strength 3 معالج الأمان revision 5، وهو تصميم مؤقت شُحن في Acrobat 9 ولم تعتمده ISO. وتقابل Strength 4 المراجعة 6، وهي المخطط ذو دالة اشتقاق المفتاح المقساة الذي يقننه ISO 32000-2. لا يوجد سبب لاختيار 3 للمستندات الجديدة؛ أما في التدقيق فالفارق مهم، لأن سياسة الامتثال التي تقول "AES-256 وفق ISO 32000-2" لا تحققها إلا R6. في جانب القراءة، تفصل طبقة الفئات بينهما كـ esAES256Bit مقابل esAES256BitAcroX، وتجيب خاصية EncryptionAcroX عن سؤال R5 أو R6 مباشرة.

بتات الأذونات وتفاصيلها المرتبطة بطول المفتاح

تعبئ EncodePermissions ثمانية أعلام داخل العدد الصحيح الذي تتوقعه Encrypt و EncryptFile: الطباعة، والنسخ، والتغيير، وإضافة الملاحظات تشكل المجموعة الأساسية، بينما ملء الحقول، والنسخ لأغراض الوصول، والتجميع، والطباعة كاملة الجودة تشكل المجموعة الممتدة. التفصيل المهم، والمذكور في عرض التشفير الخاص بالمكتبة نفسها، هو أن الأربعة الممتدة لا تُحترم إلا عند قوة 128 بت وما فوق — وكذلك خفض جودة الطباعة بضبط علم الطباعة كاملة الجودة على 0. إذا رمّزت سياسة "طباعة منخفضة الدقة فقط" في مستند 40 بت، فسيطبع العارضون ببساطة بالجودة الكاملة.

التحذير الأعمق هو من يفرض هذه البتات. الأذونات في PDF تعليمات للقراء المطابقين، لا قيود تشفيرية: مفتاح فك التشفير هو نفسه سواء سُمِح بالنسخ أم لا. مجموعة أذونات مقفلة تحافظ على التزام العارضين الملتزمين. إذا كان واجبك منع الاستخراج لا مجرد تثبيطه، فهذا يتطلب كلمة مرور مستخدم وضوابط على مستوى العملية، ويجب أن يوضح تقرير التدقيق أي النظامين يخضع له الملف.

ضبط السياسة وإثبات أنها بقيت

تطبيق التشفير على الملفات القائمة لا يتطلب تحميلها إلى شجرة الكائنات. تعالج EncryptFile ملف الإدخال إلى الإخراج في استدعاء واحد، ثم تعيد حلقة التدقيق فتح النتيجة للتحقق من المخرج — وهو نمط مأخوذ تقريبا حرفيا من العرض المشحون:

var
  PDF: TPDFlib;
  R: Integer;
begin
  PDF := TPDFlib.Create;
  try
    R := PDF.EncryptFile('in.pdf', 'out.pdf', 'owner-secret', 'user-secret', 4,
      PDF.EncodePermissions(1, 0, 0, 0,    // print allowed; copy/change/notes denied
                            0, 0, 0, 1));  // extended set: full-quality print only
    if (R = 1) and (PDF.LoadFromFile('out.pdf', 'user-secret') = 1) then
    begin
      Writeln('algorithm = ', PDF.EncryptionAlgorithm);
      Writeln('strength  = ', PDF.EncryptionStrength);
      Writeln('owner pw accepted: ', PDF.CheckPassword('owner-secret'));
    end;
  finally
    PDF.Free;
  end;
end;

الفرق التي تعمل على مستوى المستند تحصل على العملية نفسها عبر مجموعات typed بدلا من تعبئة البتات، وهذا أسهل قراءة في مراجعة الكود:

if not Doc.Encrypt('owner-secret', 'user-secret', esAES256BitAcroX,
  [ppCanPrint], [ppCanPrintFull]) then
  raise Exception.Create('Encryption failed');

في الحالتين، خطوة القراءة الراجعة ليست طقسا اختياريا. إنها تلتقط أخطاء النشر الكلاسيكية — إصدار مكتبة قديم يخفض القوة المطلوبة بصمت، أو مسار إخراج لم يُكتب أبدا، أو عدد أذونات جُمّع بترتيب معاملات خاطئ — في اللحظة التي تحدث فيها بدلا من مكتب العميل. يعطيك GetEncryptionFingerprint قيمة مدمجة لتخزينها مع سجل المهمة للمقارنة لاحقا.

إيجابيات كاذبة في التدقيق تستحق البرمجة لها

ثلاثة أنماط تنتج استنتاجات خاطئة مرارا في ماسحات الأمان. أولا، حالة crypt filter من نوع Identity من الافتتاحية: قاموس /Encrypt موجود، والمحتوى الحقيقي لم يُمس — افحص أعلام Identity لكل مرشح قبل إعلان البيانات محمية. ثانيا، انقسام البيانات الوصفية: يمكن أن يختلف EncryptMetadata مع بقية الملف في الاتجاهين، لذلك لا تقول عبارة "الملف مشفر" شيئا عن كون حزمة XMP كذلك. ثالثا، الملفات المضمنة: يسمح PDF بمرشح تشفير مخصص للمرفقات، لذلك قد تكون المرفقات هي الجزء المشفر الوحيد في مستند مفتوح، أو الجزء الوحيد بنص صريح في مستند مشفر. سجل تدقيق يلتقط تعيينات المرشحات الثلاثة بشكل منفصل — السلاسل، والدفوقات، والملفات المضمنة — محصن من الفخاخ الثلاثة؛ أما سجل يخزن قيمة منطقية فهو مخطئ في الموعد.

أسئلة يطرحها المدققون فعلا

هل أستطيع إزالة التشفير من ملف عندما أملك كلمة المرور؟ نعم — تنفذ DecryptFile(InputFileName, OutputFileName, Password) ذلك من دون تحميل كامل، وتنفذ Decrypt في المستند المحمّل الشيء نفسه في الذاكرة. أما هل يجوز لك ذلك، فهذه مسألة سياسة يجب أن تجيب عنها قواعد الاستقبال صراحة.

أي قوة يجب أن تستخدمها المستندات الجديدة؟ Strength 4، أي AES-256 revision 6، إلا إذا كان عليك دعم عارضين أقدم من Acrobat X. تبقى Strength 2 (AES-128) الحد العملي الأدنى لأساطيل العارضين القديمة جدا؛ خيارات RC4 موجودة لتدقيق التوافق لا للإخراج الجديد.

هل تمنع أعلام الأذونات مستخدما مصمما من نسخ النص؟ لا. يلتزم بها العارضون المطابقون، ويصوغها ISO 32000 كأذونات وصول يفرضها القراء، لا كتشفير. اربطها بكلمة مرور مستخدم عندما تكون السرية هي المتطلب الحقيقي.

قراءة إضافية

تتغذى حالة التشفير مباشرة في قرارات التوقيع — فبيئة العمل التي تتحقق من المستندات وتوقعها تحتاج إلى الانضباط نفسه في القراءة الراجعة، كما تشرح مقالة بيئة عمل الامتثال والتوقيع. وبالنسبة إلى خطوط الدفعات التي تطبق EncryptFile على آلاف المستندات الكبيرة، يوضح دليل Direct Access لملفات PDF الكبيرة كيف تحافظ على الذاكرة ثابتة أثناء ذلك.

مرجع API الكامل للتشفير موجود في صفحة منتج PDFlibPas.