تقوم بكتابة مصنف، وتشفيره بكلمة مرور، وتسليم الملف إلى زميل، ويقوم الزميل بفتحه في Excel. يطلب Excel كلمة المرور. يكتبها الزميل، ويقبلها Excel. حتى الآن يبدو التشفير صحيحًا. ثم يعرض Excel مربع حوار يفيد بأن الملف تالف ولا يمكن فتحه، أو يتم فتحه على ورقة من الخلايا التي لا معنى لها. كلمة المرور كانت صحيحة. الملف معطل على أي حال. يعد هذا أكثر أنماط الفشل إرباكًا في تشفير Office، لأن الجزء الذي يخبرك بأن كلمة المرور صحيحة والجزء الذي يحتوي على بياناتك محميان بعمليتين مختلفتين، والحصول على أحدهما صحيحًا لا يفعل شيئًا لضمان الآخر
كلا الخطأين الموصوفين هنا اتخذا هذا الشكل بالضبط. في كلتا الحالتين اجتاز المدقق ورفض النص الأساسي، مما يرسلك للبحث عن خطأ في كلمة المرور أو اشتقاق المفتاح غير الموجود أصلاً. كان الخطأ الحقيقي في المراحل التالية، في كيفية تحويل بايتات الحزمة. الخطآن مستقلان، أحدهما في مسار AES والآخر في مسار RC4، لكنهما يشتركان في مشكلة التشخيص، لذلك يجدر رؤية لماذا تعتبر النتيجة نصف الصحيحة هي أصعب نوع يمكن قراءته
لماذا لا تثبت كلمة المرور الناجحة أي شيء عن النص الأساسي
التنسيق الذي يستخدمه ملف XLSX المشفر الحديث هو ECMA-376 Standard Encryption، ويخزن شيئين مشفرين جنبًا إلى جنب. أحدهما هو EncryptionVerifier: كتلة صغيرة تحتوي على قيمة عشوائية وتجزئة (hash) تلك القيمة، مشفرة بالمفتاح المشتق من كلمة المرور. والآخر هو EncryptedPackage: حاوية zip الكاملة للمصنف، مشفرة بنفس المفتاح. يوجد المدقق (verifier) حتى يتمكن القارئ من تأكيد كلمة المرور قبل أن يبذل جهدًا على ميغابايتات من النص الأساسي. قم بفك تشفير المدقق، وتجزئة القيمة العشوائية، ومقارنتها بالتجزئة المخزنة، وإذا تطابقا فإن كلمة المرور صحيحة
الفخ هو أن المدقق والحزمة يتم تشفيرهما بواسطة استدعاءات منفصلة عبر مخازن مؤقتة (buffers) منفصلة. المفتاح المشتق بشكل صحيح سيفك تشفير المدقق بشكل صحيح بغض النظر عما يحدث للحزمة بعد ذلك. لذلك إذا كان اشتقاق المفتاح الخاص بك صحيحًا ولكن تحويل الحزمة الخاص بك خاطئ، فإن Excel يؤكد كلمة المرور من المدقق ثم يفشل في النص الأساسي. يُقرأ العَرَض على أنه "كلمة مرور صحيحة، ملف معطل"، مما يوجه التحقيق نحو مسار كلمة المرور، وهو الجزء الوحيد الذي لم يُكسر أبدًا. يتحكم نفس الفصل في حالة RC4 القديمة: يتم فحص تجزئة المدقق أولاً، والنص الأساسي الذي ينحرف عن المزامنة لا يزال يترك هذا الفحص سليمًا
الخطأ الأول: AES في ECB، وليس CBC
تحدد مواصفة [MS-OFFCRYPTO] §2.3.4.15 أن Standard Encryption يقوم بتشفير الحزمة باستخدام AES في وضع Electronic Codebook (ECB). يتم تشفير كل كتلة بحجم 16 بايت من الحزمة المبطنة (padded) بشكل مستقل بنفس المفتاح. لا يوجد تسلسل (chaining) بين الكتل ولا يوجد متجه تهيئة (initialization vector). يعد هذا خيارًا غير عادي وفقًا للمعايير الحديثة، حيث يتم تجنب ECB عادةً، ولكن قابلية التشغيل البيني ليست مكانًا لإعادة النظر في المواصفات. يقوم Excel بفك تشفير الحزمة كـ ECB، لذلك يجب على المنتج تشفيرها كـ ECB أو لن يتوافق الاثنان
كان الخطأ هو أنه تم تشفير الحزمة باستخدام AES في وضع CBC باستخدام متجه تهيئة كله أصفار. إليك سبب نجاح ذلك تقريبًا، ولماذا يعتبر "تقريبًا" هو أسوأ مكان يمكن الهبوط فيه. في CBC، يتم إجراء عملية XOR لكتلة النص العادي الأولى مع IV قبل التشفير. عندما يكون IV عبارة عن أصفار بالكامل، فإن عملية XOR لا تغير شيئًا، لذلك تنتج الكتلة الأولى من CBC مع IV صفري نفس النص المشفر تمامًا مثل ECB. من الكتلة الثانية فصاعدًا، يقوم CBC بتغذية كتلة النص المشفر السابقة في الكتلة التالية، لذلك تتباعد كل كتلة بعد الأولى عن ECB
الآن قم بتطبيق ذلك على الهيكل. تضع تخطيطات الحزمة بادئة طول بحجم 8 بايت بتنسيق little-endian في البداية تمامًا، لذلك توجد أجزاء الملف التي يفحصها Excel مبكرًا في الكتلة الأولى أو الثانية. تعني الكتلة الأولى التي تتطابق بالصدفة أن التحقق الأولي يمر بينما تفك تشفير كل كتلة لاحقة إلى ضوضاء. الإصلاح ليس دقيقًا بمجرد تسمية الوضع: قم بتشفير كل كتلة بحجم 16 بايت باستخدام ECB وأوقف التسلسل. في المحرك، يتنقل XlsEncryptStdPackage عبر المخزن المؤقت المبطن بخطوات بحجم 16 بايت ويستدعي AESEncryptECB128Block في كل منها، وهو نفس البدائي المستخدم بالفعل لكتل المدقق. يحمل المصدر تعليقًا عند الحلقة يوضح القاعدة ببساطة: CBC مع IV صفري يتطابق مع ECB فقط للكتلة الأولى، لذلك سيتم فك تشفير بقية الحزمة إلى بيانات عشوائية وسيرفضها Excel
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('report.xlsx');
// SaveAsEncrypted serializes the workbook, then runs the
// ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
// package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
raise Exception.Create('Encryption failed');
finally
Book.Free;
end;
end;
الخطأ الثاني: انحراف إعادة مفتاح RC4 عن الخطوة
يستخدم مسار .xls القديم مخطط RC4 CryptoAPI، وقاعدته مختلفة من حيث النوع. تحدد مواصفة [MS-OFFCRYPTO] §2.3.6 أنه يتم إعادة تعيين مفتاح التشفير عند كل حد كتلة بحجم 1024 بايت. يتم تقسيم الدفق إلى كتل بحجم 1024 بايت، ويتم اشتقاق مفتاح RC4 جديد للكتلة رقم 0، 1، 2، وما إلى ذلك، وداخل كل كتلة يتم استهلاك دفق المفاتيح (keystream) بشكل مستمر من بايت إلى بايت. يجب أن يتواجد شرطان ثابتان معًا: إعادة تعيين المفتاح عند كل حد، واستهلاك دفق المفاتيح دون فجوات داخل الكتلة. إن RC4 هو تشفير تدفقي (stream cipher)، لذا فإن دفق المفاتيح الخاص به هو تسلسل واحد مرتب؛ يتم تحديد البايت النوني (n-th) الذي تسحبه بعدد البايتات التي سحبتها قبله. فك التشفير هو نفس عملية XOR ضد نفس التسلسل، مما يعني أنه يجب على المنتج والمستهلك سحب نفس البايتات تمامًا في نفس المواضع تمامًا
تكمن الصعوبة كلها هنا. لا يحتوي التشفير التدفقي على إعادة مزامنة. إذا أهدرت بايتًا واحدًا من دفق المفاتيح، فسيتم إجراء عملية XOR لكل بايت بعده مقابل بايت دفق المفاتيح الخاطئ، ولا يصحح الخطأ نفسه أبدًا؛ بل يتسلسل إلى نهاية الكتلة، وبمجرد أن يكون موضع التشغيل خاطئًا، ينتقل إلى كل كتلة بعده. لقد فعل الخطأ هنا ذلك بالضبط. بدأ عداد الكتل من قيمة حارس (sentinel value) تبلغ سالب واحد، وافترض روتين التخطي أن العداد يطابق الكتلة الحالية بالفعل. بدءًا من هذا الحارس، قام بإعادة تعيين المفتاح وتشغيل كتلة كاملة بحجم 1024 بايت من دفق المفاتيح التي لم يكن ينبغي استهلاكها أبدًا، وفي هذه العملية دفع العدد المتبقي ليكون سالبًا. من تلك النقطة، كان جهاز فك التشفير خارج الطور بكتلة كاملة. المدقق، الذي تم فحصه قبل أي من هذا، ظل يمرر بنجاح، لذا بدت كلمة المرور صحيحة بينما ظهرت كل خلية بيانات كبيانات عشوائية
يوجد المنطق المصحح في TXLSDecrypterRC4. يشترك كل من Skip و Decrypt في حلقة واحدة: أعد تعيين المفتاح فقط عندما يتجاوز موضع التشغيل إلى كتلة جديدة، حيث يكون مؤشر الكتلة هو الموضع مقسومًا على REKEY_BLOCK_SIZE (1024)، ثم استهلك ما يصل إلى باقي الكتلة الحالية وليس أكثر من ذلك. يتم استدعاء MakeKey بمؤشر الكتلة، ولا يتم ذلك أبدًا بمؤشر قديم أو مؤشر حارس، ويتقدم الموضع بالعدد الدقيق للبايتات المعالجة بحيث يظل Skip و Decrypt محاذيين للطور مع المنتج. يكمن الدرس في أصغر وحدة: بايت واحد مهدر ليس خطأً صغيرًا في التشفير التدفقي، بل هو خسارة كلية لكل شيء في المراحل التالية
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
// CanReadEncrypted checks the Compound File (OLE2) signature so
// you can branch before attempting a normal Open. OpenEncrypted
// routes plain files to Open and handles the encrypted container.
if Book.CanReadEncrypted('legacy.xls') then
Book.OpenEncrypted('legacy.xls', 'S3cret!')
else
Book.Open('legacy.xls');
// read cells here
finally
Book.Free;
end;
end;
التشغيل البيني مع مواصفة مجمدة هو مطابقة حتى مستوى البايت
يُختزل كلا الخطأين في نفس المبدأ الجذري، ويجدر ذكره بحد ذاته لأنه يغير كيفية تقييمك لخيارات التصميم. عندما يكون مستهلك مخرجاتك برنامجًا خارجيًا ثابتًا لا يمكنك تغييره، فإن وضع التشفير ووتيرة إعادة تعيين المفتاح ليست تفاصيل تنفيذية يمكنك تحسينها أو تبسيطها. بل هي جزء من عقد النقل (wire contract). سيقوم Excel بفك التشفير باستخدام ECB وإعادة تعيين المفتاح على حدود 1024 بايت سواء كانت هذه الخيارات ترضيك أم لا، ومهمتك الوحيدة هي إنتاج بايتات تفك تشفيرها إلى الأصل تحت هذا الإجراء الدقيق. وضع أكثر حداثة، IV يبدو غير ضار، عداد يبدأ من حيث يبدو طبيعيًا؛ أي من هذه يعتبر خللًا في اللحظة التي ينحرف فيها عما يتوقعه القارئ. التشغيل البيني مقابل مواصفة مجمدة ليس تقريبيًا. إما أن يكون دقيقًا بالبايت أو يكون معطلاً
هذا هو السبب أيضًا في أن المدقق يعد اختبارًا مبدئيًا (smoke test) ضعيفًا بمفرده. يخبرك أن اشتقاق المفتاح يعمل، وهو أمر ضروري ولكنه بعيد عن أن يكون كافيًا. الاختبار الذي يفتح فقط ملفًا مشفرًا ويؤكد مرور كلمة المرور سيبلغ عن النجاح بينما يكون النص الأساسي غير قابل للقراءة. الاختبار الحقيقي يفك تشفير الحزمة ويقارن البايتات المستردة بالإدخال الأصلي، أو يقوم بإجراء دورة كاملة للمصنف من خلال التشفير وفك التشفير ويقرأ الخلايا مرة أخرى. يثبت المدقق كلمة المرور؛ فقط النص الأساسي يثبت التشفير
الطريقة المدعومة لقراءة وكتابة المصنفات المحمية
الواجهة العامة صغيرة. لكتابة مصنف حديث محمي بكلمة مرور، قم بتعبئة أو فتح TXLSXWorkbook واستدعِ SaveAsEncrypted مع اسم ملف وكلمة مرور؛ حيث يقوم بتسلسل (serializes) المصنف وتشغيل خط أنابيب Standard Encryption الذي صححه الإصلاح الأول، ويعود بـ 1 عند النجاح. للقراءة، استدعِ CanReadEncrypted لاختبار ما إذا كان الملف عبارة عن حاوية Compound File مشفرة، ثم تفرع: يتعامل OpenEncrypted مع المسار المشفر ويعود إلى Open للملفات العادية، ويتوفر Open مع كلمة مرور بشكل مباشر. تقع معالجة الوضع وحلقة إعادة تعيين المفتاح الموصوفة أعلاه تحت هذه الاستدعاءات؛ أنت توفر كلمة المرور واسم الملف ويطابق المحرك المواصفات نيابة عنك
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('quarterly.xlsx');
Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
// Reopen on the consumer side
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
تمت تغطية شكل المخرجات المحمية، ودفق EncryptionInfo، وكتل المدقق، وتخطيط الحزمة في جولتنا التفصيلية حول مخرجات XLSX المحمية بـ AES. للسؤال المنفصل حول قفل مستوى الورقة وكيفية تفاعل الحماية مع إعداد الصفحة والطباعة، راجع المقال حول الحماية وإعداد الصفحة والطباعة. كلاهما يعتمد على مسار التشفير الموصوف هنا، والذي يتم شحنه كجزء من مكون جداول البيانات HotXLS لـ Delphi و C++Builder جنبًا إلى جنب مع واجهات برمجة تطبيقات القراءة والكتابة والعرض التي تمت تغطيتها في مكان آخر في هذه المدونة