عندما يرفض ملف PDF بمراجعة 6 من AES-256 كلمة مرور تعرف أنها صحيحة، فالمشتبه المعتاد هو عدّاد الجولات في هاش R6 المقسى (ISO 32000-2 Algorithm 2.B). كان مكوّن PDFium لـ Delphi مصاباً بهذا الخلل بالضبط قبل v2.57.2: كان TPdf.SaveAsEncrypted يحسب الجولات من أساس خاطئ، وكان نحو 30% من الملفات التي أنتجها يرفضها FPDF_LoadDocument بخطأ FPDF_ERR_PASSWORD
الجزء المزعج ليس الحساب. الجزء المزعج أن العيب كان غير مرئي لكل اختبار يشفر ملفاً ثم يفحصه بالكود نفسه، وأن رحلة ذهاب وعودة ناجحة واحدة لم تثبت شيئاً. تستعرض هذه المقالة ما تطلبه الخوارزمية 2.B فعلاً، وأين يختبئ انزلاق جولة واحدة، ولماذا لا يفشل بعض الوقت فقط، وكيف تبني اختبار انحدار يمسك خلا مثلاً هذا قبل أن يمسكه عميل
ماذا تقول قاعدة إنهاء هاش R6 فعلاً؟
تتوقف الخوارزمية 2.B فقط بعد 64 جولة على الأقل، ثم فقط عندما تكون البايتة الأخيرة من أحدث خرج AES هو E أصغر من أو تساوي رقم الجولة ناقص 32، حيث رقم الجولة هو العدّ من 1 لعدد الجولات المنفذة فعلاً. بعد الجولة رقم N تقارن مع N. لا N − 1، ولا فهرس الجولة التالية. ذلك التعريف وحده هو سطح الخلل كله
بقية الخوارزمية آلية. ابدأ بـ K بوصفه SHA-256 لكلمة المرور والملح و (لمفتاح المالك) قيمة /U البالغة 48 بايتاً. تبني كل جولة K1 على هيئة password ‖ K ‖ UK يتكرر 64 مرة، وتشفرها بـ AES-128-CBC بلا حشو مستخدمةً أول 16 بايت من K مفتاحاً والـ 16 التالية ناقل تهيئة، وتأخذ أول 16 بايت من E بوصفها عدداً صحيحاً من 128 بتة big-endian mod 3، وتختار SHA-256 أو SHA-384 أو SHA-512 وفقاً لذلك لإنتاج K التالية. الهاش النهائي أول 32 بايت من K. تعرّفه ISO 32000-2 §7.6.4.3؛ وطول الحلقة المعتمد على البيانات مقصود، لأنه يجعل العمل لكل تخمين كلمة مرور غير قابل للتنبؤ لمكسّر GPU
هذا هو الشكل الصحيح للحلقة، مكتوباً بوصفه توضيحاً لا تنفيذاً جاهزاً للنسخ. تقف BuildK1 و AesCbcNoPad128 و FirstBlockMod3 ومساعدات الهاش محل أي بدائيات يمتلكها كودك أصلاً
// توضيح: هيكل حلقة الخوارزمية 2.B (ISO 32000-2، revision 6)
K := Sha256(Password + Salt + UK);
RoundNumber := 0; // الجولات المنفذة حتى الآن
repeat
Inc(RoundNumber); // هذه هي الجولة 1، 2، 3 ...
K1 := BuildK1(Password, K, UK); // (Password || K || UK) يتكرر 64 مرة
E := AesCbcNoPad128(Copy(K, 0, 16), Copy(K, 16, 16), K1);
case FirstBlockMod3(E) of // أول 16 بايت من E، big-endian، mod 3
0: K := Sha256(E);
1: K := Sha384(E);
2: K := Sha512(E);
end;
until (RoundNumber >= 64) and (E[High(E)] <= RoundNumber - 32);
Result := Copy(K, 0, 32);
اقرأ سطر الإنهاء والعدّاد في ذهنك. الزيادة في أعلى المتن، فبحلول وقت عمل اختبار until يحمل RoundNumber بالضبط عدد الجولات المكتملة، وهو ما تعنيه المعيار بكلمة رقم الجولة
أين يختبئ الانزياح-بواحد في نقل إلى Delphi
كان خلل مكوّن PDFium عدّاداً يبدأ من −1 مع الزيادة في أعلى الحلقة، فكانت القيمة المفحوصة في عبارة until دائماً عدد الجولات المنفذة ناقص واحداً. يختل أمران في وقت واحد: يصبح حد الـ 64 جولة 65، وعتبة البايتة أضيق بدرجة واحدة مما يقوله المعيار. وكلاهما يزيح نقطة الخروج بجولة واحدة بالضبط في الحالات التي يهم فيها
لذلك الانزلاق تنكرات عديدة كلها معقولة بالمقدار نفسه. ابدأ من 0 وزد في أسفل المتن، بعد أن كُتب الاختبار أصلاً مقابل القيمة قبل الزيادة. استخدم فهرس حلقة for صفري الأساس وأعد استخدامه في المقارنة. ترجم مرجعاً من C تُفحص فيه متغير الحلقة قبل ++. كل منها يقرأ بشكل طبيعي في مراجعة الكود، وكل منها ينتج هاشاً يبدو عشوائياً تماماً، لأنه كذلك: إنه مجرد خرج SHA مختلف وبذو مظهر صالح لا يحسبه قارئ آخر. يحمل CalculateR6Hash المصلح في FPdfEncrypt.pas الآن تعليقاً يثبّت الدلالة من 1، بالضبط لأن النسخة الخاطئة لا تبدو خاطئة
لماذا يفشل ملف AES-256 R6 بعض الوقت فقط؟
الفشل احتمالي لأن الجولة الإضافية تغيّر النتيجة فقط عندما تهبط البايتة المنهية في نافذة الجولة الواحدة بين العتبة الصحيحة والمزاحة. في كل حالة أخرى تخرج النسختان في الجولة نفسها وتنتجان K نفسه، فيفتح الملف جيداً. في قياس مكوّن PDFium أنتج 18 من 60 تشفيراً (نحو 30%) قيم /U و /O و /UE و /OE تخالفت تنفيذاً معيارياً؛ وكانت الـ 70% الأخرى صحيحة بايتاً-ببايت
تُسحب أملاح عشوائية طازجة لكل حفظ، فينجح المستند نفسه وكلمة المرور نفسها في تشغيل ويفشل في التالي. ومن مكتب دعم يبدو ذلك تخزيناً متقلباً أو تحميلاً تالفاً أو مستخدماً يخطئ كتابة كلمة المرور. لا أحد يفكر في اتهام عدّاد حلقة حين يفتح سبعة من كل عشرة ملفات. إذا رأى منتجك تقارير "كلمة مرور خاطئة" متقطعة على ملفات PDF شفّرها كودك أنت، وفقط على المراجعة 6، فهذا أول مكان تنظر إليه
المراجعة 5 محصّنة، لأنها تستخدم مرور SHA-256 واحداً بلا تكرار. أما مسار AESV4 من ISO/TS 32003 فليس كذلك: تعيد قيمة erR7 من TPdfEncryptionRevision استخدام الخوارزمية 2.B لهاشات كلمات المرور الخاصة بها، فيغطي الإصلاح نفسه ملفات GCM الموصوفة في تشفير AES-256-GCM وسلامة PDF MAC مع ISO/TS 32003
فخ الاتساق الذاتي: لماذا تنجح اختبارات الذهاب والإياب
رحلة الذهاب والإياب عبر مشفّرك ومتحققك أنت تنجح دائماً، لأن الجانبين يشغلان الحلقة الخاطئة نفسها ويتفاقان على الهاش الخاطئ نفسه. العيب لا يوجد إلا نسبةً إلى تنفيذ شخص آخر، فالحكم لا بد أن يكون مستقلاً: محرك PDFium في pdfium.dll، أو Acrobat، أو qpdf، أو أي قارئ لم تكتبه
هذا هو الدرس العام غير المريح لأي أحد يكتب كود مستندات مجاوراً للتعمية. اختبارات الوحدة التي تحسب هاشاً وتقارنه بخرج الدالة قيد الاختبار تثبت الحتمية لا الصحة. متجهات الإجابات المعروفة من المعيار أو أداة مرجعية أفضل، ورحلة الذهاب والإياب عبر التنفيذات هي أقرب شيء إلى الحقيقة الأرضية تحصل عليه. يتمتع مكوّن PDFium بموضع محظوظ هنا: التشفير يتم في Pascal عبر EncryptDocumentBytes، بينما يمر فك التشفير عند التحميل عبر معالج الأمان C++ الخاص بـ PDFium نفسه، فإعادة فتح الملف المحفوظ بـ TPdf فحص مستقل حقيقي لا مرآة
uses
SysUtils, PDFium, FPdfEncrypt;
procedure SaveR6(const Source, Target: string);
var
Pdf: TPdf;
Opts: TPdfEncryptOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := Source;
Pdf.Active := True;
Opts := TPdfEncryptOptions.Default;
Opts.UserPassword := 'userpw';
Opts.OwnerPassword := 'ownerpw';
Opts.Revision := erR6; // AESV3، هاش كلمة مرور الخوارزمية 2.B
if not Pdf.SaveAsEncrypted(Target, Opts) then
raise Exception.Create('SaveAsEncrypted failed');
finally
Pdf.Free;
end;
end;
يبدأ TPdfEncryptOptions.Default من المراجعة 5 بكل الأذونات مسموحة والبيانات الوصفية مشفرة، فاضبط Revision صراحةً عندما تريد R6. ويأخذ TPdf.SaveAsEncryptedToStream سجل الخيارات نفسه عندما يذهب الإخراج إلى TStream بدلاً من ملف. لاحظ أن كلمات المرور AnsiString في سجل الخيارات، وهذا مهم إن كنت تمرر بيانات اعتماد غير ASCII؛ طبّعها وشفّرها قبل أن تصل إلى السجل
كيف تختبر انحداراً لخلل تشفير احتمالي؟
كرر رحلة الذهاب والإياب عبر التنفيذات مرات كافية حتى يستحيل على معدل فشل 30% أن يتسلل. يفوّت تشغيل واحد خلل مكوّن PDFium نحو 70% من الوقت. عشرون تشغيلاً كل منها بأملاح طازجة ينزل معدل الفوات إلى نحو 0.08% (0.7 مرفوعة للقوة 20)، وهذا سبب دوران اختبار الانحدار المشحون 20 مرة بدلاً من مرة
procedure TEncryptTests.R6_EncryptedDocumentReopensWithUserPassword;
var
Pdf: TPdf;
I: Integer;
Target: string;
begin
Target := TempOutputPath('encrypt_r6_roundtrip.pdf');
for I := 1 to 20 do
begin
SaveR6(SamplePdf, Target); // أملاح طازجة في كل حفظ
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'userpw';
Pdf.FileName := Target;
Pdf.Active := True; // PDFium يتحقق من /U باستقلالية
AssertTrue('Run ' + IntToStr(I) + ' must reopen', Pdf.Active);
finally
Pdf.Free;
end;
end;
end;
اقرن ذلك بالحالة السالبة، كلمة مرور خاطئة لا بد أن تفشل، حتى لا يستطيع محمِّل معطوب أن يجعل الاختبار الموجب ينجح إفراغاً. وإذا بنت بـ Lazarus، فأعد بناء حزمة المكتبة قبل تشغيل مشروع الاختبار بعد أن تلمس Lib\*.pas؛ يربط بناء الاختبار وحدات .ppu المجمعة مسبقاً للحزمة، والتشغيل على كود كائن قديم يعطيك أخضر مطمئناً على خلل لم تصلحه فعلاً. والتغذية العشوائية فخ ذو صلة يستحق المراجعة في المرور نفسه: دالة Random في FPC تنتج مفاتيح تشفير ثابتة حالة أخرى يكون فيها الخرج يبدو عشوائياً وهو خاطئ بهدوء
الأسئلة الشائعة
هل يصيب خلل عدّاد جولات R6 الملفات المشفرة بالمراجعة 5؟
لا. تهاش المراجعة 5 كلمة المرور بمرور SHA-256 واحد ولا عدّ تكرار لديها، فلا عدّاد جولات ليخطئ فيه. فقط المراجعة 6 ومراجعة AESV4 من ISO/TS 32003، وكلاهما يستخدم الخوارزمية 2.B، مصابتان
هل ما يزال يمكن فتح ملف كتبه الإصدار المعطوب؟
نحو 70% منها يفتح بشكل طبيعي لأن هاشه صادف المطابقة. والبقية تحمل قيم /U و /O لا يعيد إنتاجها أي قارئ معياري، فيفشل فحص كلمة المرور في كل مكان؛ والإصلاح العملي هو إعادة التشفير من المصدر غير المشفر بـ v2.57.2 أو أحدث
لماذا الاختبار مقابل PDFium بدلاً من كود فك التشفير لديك؟
يتشارك فاكّ التشفير الخاص بك تنفيذ الخوارزمية 2.B نفسه مع مشفّرك، فيُلغى عدد الجولات الخاطئ ذاته وتنجح كل رحلة ذهاب وإياب. تنفيذ مستقل كمعالج الأمان في PDFium هو الفحص الوحيد القادر على أن يخالفك
التشفير وتحقق PDF MAC وبقية خط أنابيب الحفظ يأتيان ضمن مكوّن PDFium لـ Delphi و C++Builder، مع المصدر واختبارات الانحدار لـ Delphi و Lazarus معاً