Technical Article

لماذا يرفض Excel كتيب العمل المشفر الخاص بك: ECB و RC4

تكتب كتيب عمل، وتشفره بكلمة مرور، وتسلم الملف إلى زميل، ويفتحه الزميل في Excel. يطلب Excel كلمة المرور. يكتبها الزميل، ويقبلها Excel. حتى الآن يبدو التشفير صحيحاً. ثم يظهر Excel مربع حوار يقول إن الملف تالف ولا يمكن فتحه، أو يفتح على ورقة مليئة بخلايا لا معنى لها. كانت كلمة المرور صحيحة. الملف معطل على أي حال. هذا هو وضع الفشل الأكثر إرباكاً في تشفير Office، لأن الجزء الذي يخبرك بأن كلمة المرور صحيحة والجزء الذي يحتوي على بياناتك محميان بعمليتين مختلفتين، والحصول على إحداهما صحيحة لا يضمن الأخرى على الإطلاق.

كلا الخللين الموصوفين هنا لهما هذا الشكل تماماً. في كل حالة، نجح مدقق الصحة وفشل الجسم، مما يرسلك للبحث عن خلل في كلمة المرور أو اشتقاق المفاتيح وهو ليس هناك. كان الخطأ الحقيقي في المراحل اللاحقة، في كيفية تحويل بايتات الحزمة. الخطآن مستقلان، أحدهما في مسار AES والآخر في مسار RC4، لكنهما يشتركان في مشكلة تشخيص، لذا من الجدير بالذكر معرفة سبب كون النتيجة نصف الصحيحة هي الأصعب في القراءة.

لماذا لا يثبت مرور كلمة المرور شيئاً عن الجسم

التنسيق الذي يستخدمه ملف XLSX المشفر الحديث هو تشفير معيار ECMA-376 (Standard Encryption)، وهو يخزن شيئين مشفرين جنباً إلى جنب. أحدهما هو EncryptionVerifier: كتلة صغيرة تحمل قيمة عشوائية وتجزئة (hash) لتلك القيمة، مشفرة بالمفتاح المشتق من كلمة المرور. والآخر هو EncryptedPackage: حاوية zip الكاملة لكتيب العمل، مشفرة بنفس المفتاح. يوجد المدقق حتى يتمكن القارئ من تأكيد كلمة المرور قبل أن يبذل جهداً في ميجابايتات من الجسم. قم بفك تشفير المدقق، وتجزئة القيمة العشوائية، ومقارنتها بالتجزئة المخزنة، وإذا تطابقت فإن كلمة المرور صحيحة.

المصيدة هي أن المدقق والحزمة يتم تشفيرهما بواسطة استدعاءات منفصلة عبر حاويات منفصلة. المفتاح المشتق بشكل صحيح سيفك تشفير المدقق بشكل صحيح بغض النظر عما يحدث للحزمة بعد ذلك. لذا، إذا كان اشتقاق المفتاح صحيحاً ولكن تحويل الحزمة خاطئاً، فإن Excel يؤكد كلمة المرور من المدقق ثم يفشل في الجسم. تقرأ الأعراض كـ "كلمة مرور صحيحة، ملف معطل"، مما يوجه التحقيق إلى مسار كلمة المرور، وهو الجزء الوحيد الذي لم يتعتعطل أبداً. نفس الفصل يحكم حالة RC4 القديمة: يتم فحص تجزئة المدقق أولاً، والجسم الذي ينحرف خارج المزامنة لا يزال يترك هذا الفحص سليماً.

الخلل الأول: AES في وضع ECB، وليس CBC

تحدد المواصفة [MS-OFFCRYPTO] §2.3.4.15 أن التشفير القياسي يشفر الحزمة باستخدام AES في وضع كتاب الشفرات الإلكتروني (Electronic Codebook - ECB). يتم تشفير كل كتلة مكونة من 16 بايت من الحزمة المبطنة بشكل مستقل بنفس المفتاح. لا يوجد تسلسل بين الكتل ولا يوجد متجه تهيئة (IV). هذا خيار غير معتاد بالمعايير الحديثة، حيث يتم تجنب ECB عادة، لكن التشغيل البيني ليس مكاناً للتخمين في المواصفات. يقوم Excel بفك تشفير الحزمة كـ ECB، لذا يجب على المنتج تشفيرها كـ ECB وإلا فلن يتفق الطرفان.

كان الخلل هو أن الحزمة تم تشفيرها باستخدام AES في وضع CBC باستخدام متجه تهيئة صفري بالكامل. وإليك سبب نجاح ذلك تقريباً، ولماذا يعد التقارب هو أسوأ مكان للهبوط. في وضع CBC، يتم عمل عملية XOR للكتلة النصية الأولى مع متجه التهيئة قبل التشفير. عندما يكون متجه التهيئة صفرياً بالكامل، فإن عملية XOR لا تغير شيئاً، لذا فإن الكتلة الأولى من CBC مع متجه تهيئة صفري تنتج بالضبط نفس النص المشفر مثل وضع ECB. ومن الكتلة الثانية فصاعداً، يغذي وضع CBC كتلة النص المشفر السابقة في التالية، لذا فإن كل كتلة بعد الأولى تنحرف عن وضع ECB.

الآن ضع ذلك فوق الهيكل. يضع تخطيط الحزمة بادئة طول بحجم 8 بايتات ذات ترتيب طرفي صغير (little-endian) في البداية تماماً، لذا فإن أجزاء الملف التي يفحصها Excel أولاً تقع في الكتلة الأولى أو الثانية. تعني الكتلة الأولى التي تتطابق مصادفة أن أول عملية تحقق تنجح بينما يتم فك تشفير كل كتلة لاحقة إلى ضوضاء. الإصلاح ليس غامضاً بمجرد تسمية الوضع: قم بتشغيل كل كتلة مكونة من 16 بايت باستخدام ECB وتوقف عن التسلسل. في المحرك، يمر XlsEncryptStdPackage عبر الحاوية المبطنة بخطوات مكونة من 16 بايت ويستدعي AESEncryptECB128Block في كل منها، وهو نفس البدائي المستخدم بالفعل لكتل المدقق. يحمل المصدر تعليقاً عند الحلقة يذكر القاعدة بوضوح: يطابق وضع CBC مع متجه تهيئة صفري وضع 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 أن التشفير يتم إعادة مفاتيحه (re-keyed) عند كل حد كتلة بحجم 1024 بايت. ينقسم التدفق إلى كتل بحجم 1024 بايت، ويتم اشتقاق مفتاح RC4 جديد للكتل رقم 0 و 1 و 2 وهكذا، وداخل كل كتلة يتم استهلاك تدفق المفاتيح باستمرار من بايت إلى بايت. يجب أن يستقر متغيران معاً: إعادة المفاتيح عند كل حد، واستهلاك تدفق المفاتيح دون فجوات داخل الكتلة. يعد تشفير RC4 تشفيراً تدفقياً، لذا فإن تدفق المفاتيح الخاص به هو سلسلة مرتبة واحدة؛ ويتم تحديد البايت n الذي تسحبه بعدد البايتات التي سحبتها قبله. فك التشفير هو نفس عملية XOR ضد نفس السلسلة، مما يعني أنه يجب على المنتج والمستهلك سحب نفس البايتات تماماً في نفس المواضع تماماً.

هذه هي الصعوبة الكاملة. لا يحتوي التشفير التدفيقي على إعادة مزامنة. إذا أهدرت بايتاً واحداً من تدفق المفاتيح، فإن كل بايت بعده يتم عمل عملية XOR له ضد بايت تدفق المفاتيح الخاطئ، ولا يصلح الخطأ نفسه أبداً؛ بل يتالي إلى نهاية الكتلة، وبمجرد أن يكون الموضع قيد التشغيل خاطئاً، إلى كل كتلة تليها. فعل الخلل هنا هذا تماماً. بدأ عداد الكتل من قيمة حارس سالبة، وافترض روتين التخطي أن العداد يطابق بالفعل الكتلة الحالية. بدءاً من هذا الحارس، أعاد المفاتيح وقام بتشغيل كتلة كاملة بحجم 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;

التشغيل البيني مع مواصفات مجمدة هو مطابقة للبايت

يختزل كلا الخللين إلى نفس المبدأ الجذري، ومن الجدير بالذكر ذكره بمفرده لأنه يغير كيفية وزنك لخيارات التصميم. عندما يكون مستهلك مخرجاتك برنامجاً خارجياً ثابتاً لا يمكنك تغييره، فإن وضع التشفير ووتيرة إعادة المفاتيح ليست تفاصيل تنفيذ يمكنك تحسينها أو تبسيطها. بل هي جزء من عقد السلك. سيقوم Excel بفك التشفير باستخدام ECB وإعادة المفاتيح عند حدود 1024 بايت سواء كانت تلك الخيارات تسعدك أم لا، ومهمتك الوحيدة هي إنتاج بايتات تفك تشفيرها إلى الأصل تحت هذا الإجراء الدقيق. وضع أكثر حداثة، ومتجه تهيئة (IV) يبدو غير ضار، وعداد يبدأ من حيث يشعر أنه طبيعي؛ أي من هذه يعد عيباً في اللحظة التي ينحرف فيها عما يتوقعه القارئ. التشغيل البيني ضد مواصفات مجمدة ليس تقريبياً. إنه دقيق على مستوى البايت أو أنه معطل.

هذا هو السبب أيضاً في أن المدقق يعد اختباراً أولياً سيئاً بمفرده. فهو يخبرك أن اشتقاق المفتاح يعمل، وهو أمر ضروري ولكنه بعيد عن الكفاية. الاختبار الذي يفتح فقط ملفاً مشفراً ويؤكد مرور كلمة المرور سيبلغ عن النجاح بينما الجسم غير قابل للقراءة. فك التشفير الحقيقي يفك تشفير الحزمة ويقارن البايتات المستردة بالمدخلات الأصلية، أو يعالج كتيب العمل ذهاباً وإياباً من خلال التشفير وفك التشفير ويقرأ الخلايا مرة أخرى. يثبت المدقق كلمة المرور؛ والجسم فقط هو ما يثبت التشفير.

الطريقة المدعومة لقراءة وكتابة كتيبات العمل المحمية

السطح العام صغير. لكتابة كتيب عمل حديث محمي بكلمة مرور، قم بملء أو فتح TXLSXWorkbook واستدعِ SaveAsEncrypted مع اسم الملف وكلمة المرور؛ حيث يقوم بتسلسل كتيب العمل وتشغيل خط أنابيب التشفير القياسي (Standard Encryption) الذي صححه الإصلاح الأول، مرجعاً 1 عند النجاح. للقراءة، استدعِ CanReadEncrypted لاختبار ما إذا كان الملف عبارة عن حاوية ملف مركب مشفر، ثم تفرع: يتعامل 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 جنباً إلى جنب مع واجهات برمجة التطبيقات للقراءة والكتابة والرسم المغطاة في مكان آخر في هذه المدونة.