يقرأ HotXLS ملفات Excel المشفرة بأسلوب Agile، وهي حماية كلمة المرور التي يطبقها برنامج Excel 2010 وكل إصدار لاحق بشكل افتراضي، من خلال استدعاء واحد: TXLSXWorkbook.OpenEncrypted. ويحلل المكون واصف تشفير XML، ويشتق المفاتيح من كلمة المرور بسلسلة تجزئة SHA-512 مع عدد الدورات (spin-count)، ويتحقق من كلمة المرور مقابل المدقق المشفر، ثم يفك تشفير الحزمة في قطاعات AES-CBC بحجم 4096 بايت. ولا يتدخل في ذلك أي تثبيت لـ Excel، أو COM، أو مكتبة DLL خارجية للتشفير
يغطي هذا المقال جانب القراءة لتشفير Agile على وجه التحديد. وهناك مشكلتان مجاورتان لهما مقالاهما الخاصان: يتم تغطية التفاعل مع أنظمة RC4 و XOR القديمة داخل ملفات BIFF .xls القديمة في مقال التفاعل بين ECB و RC4، ويتم تغطية إنتاج مصنفات محمية بكلمة مرور مع تشفير ECMA-376 القياسي في مقال مخرجات XLSX المحمية بـ AES. وهنا يكون الملف موجودًا بالفعل، وقام شخص آخر بتشفيره، ومهمتك هي فتحه
السيناريو الذي يفرض هذه المشكلة مألوف لأي شخص يدير خط أنابيب للمستندات. فتستقبل خدمة استيراد من جانب الخادم عمليات تحميل المصنفات؛ ولا يوجد برنامج Excel على الجهاز ولن يوجد أبدًا؛ وفي صباح أحد الأيام، يقوم العميل بتحميل ملف .xlsx عادي تمامًا يرفضه قارئ ZIP لأنه ليس ملف ZIP على الإطلاق. لقد حفظه العميل بكلمة مرور. ومن تلك اللحظة، إما أن يفهم برنامج التحميل الخاص بك مواصفات [MS-OFFCRYPTO] أو يعيد الملف إلى المستخدم الذي لم يفعل، من وجهة نظره، أي شيء غير عادي
ما هو تشفير Agile في ملف Excel؟
تشفير Agile هو مخطط حماية كلمة المرور المحدد في [MS-OFFCRYPTO] من الفقرة §2.3.4.10 إلى §2.3.4.15، وهو ما يكتبه برنامج Excel 2010 والإصدارات الأحدث عندما يتم حفظ مصنف بكلمة مرور. ولم يعد الملف المشفر حزمة ZIP، بل هو حاوية ملف مركب OLE ثنائي (CFB) تحمل تدفقين: EncryptionInfo، الذي يصف كيفية إجراء التشفير، و EncryptedPackage، وهو ملف ZIP الحقيقي .xlsx المشفر ككتلة مبهمة (blob). وتوقيع CFB البنية (D0 CF 11 E0 A1 B1 1A E1) هو نفس التوقيع السحري الذي تحمله ملفات BIFF .xls القديمة، ولهذا السبب لا يمكن تصنيف الملف المعاد تسميته أو المشفر بالامتداد وحده
ما يميز تشفير Agile عن سابقيه هو أن EncryptionInfo يصف نفسه بنفسه. فبعد بادئة إصدار بحجم 8 بايت، حيث يكون كلا الإصدارين الرئيسي والفرعي 4، يكون التدفق واصف XML بترميز UTF-8. ويعلن عنصر keyData عن التشفير (AES)، ووضع السلسلة (ChainingModeCBC)، والتجزئة (SHA512)، وطول المفتاح بالبتات، وحجم الكتلة، وملح Base64. ويحمل عنصر keyEncryptor لكلمة المرور ملحه الخاص، و spinCount، وثلاث حمولات Base64: هي encryptedVerifierHashInput، و encryptedVerifierHashValue، و encryptedKeyValue. ويكتب Excel تشفير AES-256 مع عدد دورات 100,000، ولكن يُسمح للواصف بالإعلان عن AES-128 أو AES-192، ويحترم HotXLS ما تنص عليه keyBits بدلاً من افتراض القيمة 256
نقطة دخول واحدة للمصنفات النصية العادية والقياسية والمشفرة بأسلوب Agile
تتعامل الدالة TXLSXWorkbook.OpenEncrypted مع الحالات الثلاث التي يمكن أن يواجهها المستدعي: ملف ZIP العادي، والمشفر بأسلوب القياسي، والمشفر بأسلوب Agile، وبالتالي لا تحتاج معالجات التحميل إلى تصنيف الملفات قبل تحميلها. وتتحقق الطريقة أولاً من الملف: فإذا لم يكن هناك توقيع CFB، فإنها تتراجع إلى مسار Open العادي ويتم ببساطة تجاهل كلمة المرور. وإذا كان الملف حاوية CFB، فإنها تجرب تشفير ECMA-376 القياسي أولاً، وعندما يكون توقيع إصدار EncryptionInfo هو Agile 4.4، فإنها ترسل العمل إلى خط أنابيب Agile. وتكون القيمة المرجعة هي 1 عند النجاح، وهو نفس عقد Open
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
// Works for plain .xlsx, Standard-encrypted and
// Agile-encrypted files alike
if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
finally
Wb.Free;
end;
end;
التراجع عن المدخلات غير المشفرة يهم أكثر مما يبدو. فالمستورد الدفعي (batch importer) الذي يستدعي دائمًا OpenEncrypted لا يحتاج إلى تفريع عند موقع الاستدعاء: فالملفات التي لم تكن محمية أبدًا يتم تحميلها تمامًا كما كان قبل، والملفات التي تصل مشفرة يتم فك تشفيرها في مكانها ثم تغذيتها إلى قارئ ZIP العادي كتدفق في الذاكرة. وهناك مسار كود واحد لاختباره، وليس ثلاثة
كيف تصبح كلمة المرور مفتاح AES؟
لا يستخدم تشفير Agile كلمة المرور مباشرة أبدًا. ويقوم HotXLS أولاً بحساب تجزئة مكررة: التلخيص الأولي هو SHA-512 على ملح كلمة المرور المدمج مع بايتات UTF-16LE لكلمة المرور، ثم تتم إعادة تجزئة التلخيص spinCount من المرات، حيث تسبق كل جولة عداد التكرار الصغير 32 بت للتلخيص السابق. ومع عدد دورات Excel الافتراضي البالغ 100,000، فإن ذلك يعني مئة ألف استدعاء متسلسل لـ SHA-512 لكل محاولة كلمة مرور، وهذا هو المغزى بالكامل. وعدد الدورات هو خانق للقوة الغاشمة: فهو يكلف المستدعي الشرعي بضع أجزاء من الثانية لمرة واحدة، ويكلف مهاجم القاموس نفس الأجزاء من الثانية لكل تخمين فردي
// [MS-OFFCRYPTO] iterated password hash:
// H(0) = SHA-512(salt + UTF-16LE(password))
// H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
function AgilePasswordHash(const Password: WideString;
const Salt: TBytes; SpinCount: Integer): TBytes;
var
buf: TBytes;
i: Integer;
begin
Result := XlsSHA512(Concat(Salt, Utf16LEBytes(Password)));
SetLength(buf, 4 + 64);
for i := 0 to SpinCount - 1 do
begin
PutLE32(buf, 0, i); // iteration counter, little-endian
Move(Result[0], buf[4], 64); // previous digest
Result := XlsSHA512(buf);
end;
end;
التجزئة المكررة ليست مفتاحًا بعد. فثلاثة مفاتيح مختلفة تشتق منها بتجزئتها مرة أخرى مع ملحق مفتاح كتلة ثابت 8 بايت، وهو ثابت لكل غرض: FE A7 D2 76 3B 4B 9E 79 لفك تشفير إدخال المدقق، و D7 AA 0F 6D 30 61 34 4E لتجزئة المدقق، و 14 6E 0B E7 AB AC D0 D6 لفك مفتاح الحزمة الفعلي. ويتم اقتطاع كل نتيجة SHA-512 إلى طول المفتاح المعلن، ويتم حشوها ببايتات 0x36 في الحالة النظرية حيث تكون التجزئة أقصر من المفتاح، وفقًا للمواصفة [MS-OFFCRYPTO]. وينطبق نفس قانون حشو 0x36 عندما يتم توسيع ملح كلمة المرور إلى حجم الكتلة لاستخدامه كمتجه التهيئة IV لـ CBC
التحقق من كلمة المرور وفخ اقتطاع saltSize
يتحقق HotXLS من كلمة المرور قبل أن يلمس الحزمة، باستخدام زوج المدقق من الواصف. ويقوم بفك تشفير encryptedVerifierHashInput بالمفتاح المشتق الأول، وتجزئة النتيجة بـ SHA-512، وفك تشفير encryptedVerifierHashValue بالمفتاح المشتق الثاني، ومقارنة التلخيصين بايت ببايت. ويعني عدم التطابق أن كلمة المرور خاطئة، ويتم الإبلاغ عن ذلك كشهادة نتيجة مميزة بدلاً من مصنف مشوه، والأهم من ذلك أنه يعني عدم فك تشفير جسم الحزمة بمفتاح سيئ، وبالتالي لا يوجد سيناريو تنتج فيه كلمة المرور الخاطئة بيانات تالفة تبدو معقولة
هناك تفصيل في المواصفات هنا يسهل فهمه بشكل خاطئ. فالمواصفة [MS-OFFCRYPTO] §2.3.4.13 تحدد المدقق كـ saltSize بايت من البيانات العشوائية، حيث saltSize هو طول ملح مشفر المفتاح، وليس حجم كتلة التشفير. ونظرًا لأن نص تشفير AES-CBC محاذى للكتلة، فإن إدخال المدقق الذي تم فك تشفيره يرجع محشوًا لمضاعف 16 بايت، ويجب اقتطاعه إلى saltSize قبل التجزئة. ويكتب Excel دائمًا saltSize مساويًا لـ blockSize، وكلاهما 16، لذا فإن التنفيذ الذي يتخطى الاقتطاع يجتاز كل اختبار ضد مخرجات Excel الحقيقية ثم يفشل في أول ملف من منتج اختار طول ملح مختلف. ويقوم HotXLS بالاقتطاع إلى طول الملح لأن هذا هو ما تنص عليه المواصفات بالفعل، وتوافق القيمتين في الممارسة العملية هو صدفة وليس عقدًا
كيف يتم فك تشفير الـ EncryptedPackage؟
يبدأ تدفق EncryptedPackage بحجم نص واضح بحجم 8 بايت بطريقة الطرف الأصغر (little-endian)، متبوعًا بنص التشفير في قطاعات بحجم 4096 بايت، ويقوم HotXLS بفك تشفيرها قطاعًا بعد قطاع مع متجه IV جديد لكل قطاع. ومفتاح الحزمة نفسه لا يشتق من كلمة المرور: فهو مفتاح وسيط عشوائي قام الكاتب بتشفيره في encryptedKeyValue، ويقوم HotXLS بفتحه بالمفتاح المشتق الثالث، مقتطعًا إلى طول المفتاح الذي يعلنه keyData. ومتجه IV لكل قطاع هو SHA-512 على ملح keyData المدمج مع فهرس القطاع الصغير 32 بت، مقتطعًا إلى حجم الكتلة. وهذا البناء يعني إمكانية فك تشفير أي قطاع بحجم 4096 بايت بشكل مستقل، وهو ما يجعل التنسيق صديقًا للوصول العشوائي من حيث المبدأ، على الرغم من أن HotXLS يفك تشفير الحزمة بأكملها إلى الذاكرة ويسلم بايتات ZIP الناتجة إلى قارئ XLSX العادي
يقوم حجم النص الواضح المعلن بالجزء الأخير من العمل. وتكون مخرجات AES-CBC محاذية للكتلة، لذا فإن القطاع الأخير يحمل ما يصل إلى 15 بايت من الحشو التي ليست جزءًا من المستند؛ ويتم اقتطاع المخزن المؤقت الذي تم فك تشفيره إلى بادئة الحجم، وتكون النتيجة هي بالضبط ملف ZIP لـ .xlsx الذي قام Excel بتشفيره. ويتحقق HotXLS من صحة البادئة مقابل طول التدفق الفعلي قبل فك التشفير، بحيث يفشل التحميل المقتطع أو حقل الحجم المتلاعب به بشكل نظيف بدلاً من التجاوز
الإبلاغ عن الأخطاء والحدود الصادقة
يتم الاحتفاظ بأوضاع الفشل منفصلة عمدًا. فكلمة المرور الخاطئة تثير استثناءً مع رسالة كلمة مرور خاطئة صريحة، مدفوعة بعدم تطابق المدقق، بحيث يمكن لواجهة المستخدم مطالبة المستخدم بإعادة المحاولة. وحاوية CFB التي يعلن واصفها عن خوارزميات خارج المجموعة المدعومة، أي شيء غير AES مع سلسلة CBC وتجزئة SHA-512 في واصف Agile، أو حاوية ليست قياسية ولا Agile، تثير استثناءً مختلفًا يحدد المخطط كغير مدعوم. ويجب ألا يتم الخلط بين الاثنين أبدًا: فإعادة محاولة كلمة المرور ضد مخطط غير مدعوم تضيع وقت المستخدم، والإبلاغ عن كلمة مرور خاطئة كخطأ في التنسيق يرسل فريق الدعم الخاص بك في الطريق الخطأ
function LoadUploadedWorkbook(const FileName: WideString;
const Password: WideString; Wb: TXLSXWorkbook): Boolean;
begin
Result := False;
try
Result := Wb.OpenEncrypted(FileName, Password) = 1;
except
on E: EXlsxEncryptionNotImplemented do
// Raised for both a wrong password and an unsupported
// scheme; E.Message states which, so log it verbatim and
// only offer a password retry for the wrong-password case
RejectUpload(FileName, E.Message);
end;
end;
الحدود تستحق التصريح بها بوضوح. يقرأ HotXLS واصفات Agile التي تعلن عن AES في وضع CBC مع SHA-512، وهو ما يغطي ما يكتبه Excel 2010 إلى Excel 365 بالفعل، في جميع أحجام المفاتيح الثلاثة. ويتم رفض الواصفات التي تعلن عن شفرات أو خوارزميات تجزئة أخرى بدلاً من التخمين فيها، ولا يتم استشارة مشفري المفاتيح المستندة إلى الشهادات، بل مشفر مفتاح كلمة المرور فقط. وعلى جانب الكتابة، ينتج HotXLS حاليًا تشفيرًا قياسيًا بدلاً من Agile، وهو تمييز مهم إذا كانت الأدوات اللاحقة تفحص المخطط؛ والتفاصيل موجودة في المقال الخاص بكتابة مخرجات XLSX المحمية بـ AES
تتوقف عمليات التحميل المحمية بكلمة مرور عن كونها حالة خاصة بمجرد أن يتعامل برنامج التحميل مع التشفير كجزء من تنسيق الملف وليس كاستثناء له. وتشحن نقطة الدخول OpenEncrypted، واشتقاق عدد دورات SHA-512، وخط أنابيب AES-CBC المجزأ الموصوف هنا كجزء من مكون HotXLS لدلفي و Excel، إلى جانب بقية محرك قراءة وكتابة XLS و XLSX الأصلي لدلفي و C++Builder