مقاله فنی

خواندن فایل‌های اکسل رمزگذاری‌شده Agile در دلفی با HotXLS

HotXLS فایل‌های اکسل رمزگذاری‌شده با طرح Agile را، همان محافظت با رمز عبوری که اکسل ۲۰۱۰ و هر نسخه پس از آن به‌صورت پیش‌فرض اعمال می‌کنند، با یک فراخوانی می‌خواند: TXLSXWorkbook.OpenEncrypted. کامپوننت توصیف‌گر XML رمزگذاری را تجزیه می‌کند، کلیدها را با یک زنجیره هش SHA-512 با شمارش چرخش از رمز عبور استخراج می‌کند، رمز عبور را در برابر راستی‌آزمای رمزگذاری‌شده تأیید می‌کند و سپس بسته را در بخش‌های ۴۰۹۶ بایتی AES-CBC رمزگشایی می‌کند. هیچ نصبی از اکسل، هیچ COM و هیچ DLL رمزنگاری خارجی در کار نیست

این مقاله به‌طور مشخص سمت خواندن رمزگذاری Agile را پوشش می‌دهد. دو مسئله همسایه مقاله‌های خود را دارند: تعامل‌پذیری با طرح‌های قدیمی RC4 و XOR درون فایل‌های BIFF .xls در مقاله تعامل‌پذیری ECB و RC4 پوشش داده شده، و تولید کارپوشه‌های محافظت‌شده با رمز عبور به کمک رمزگذاری استاندارد ECMA-376 در مقاله خروجی XLSX محافظت‌شده با AES. اینجا فایل از قبل وجود دارد، کس دیگری آن را رمزگذاری کرده و کار شما بازکردن آن است

سناریویی که این مسئله را تحمیل می‌کند برای هر کسی که یک خط لوله سند را اداره می‌کند آشناست. یک سرویس واردسازی سمت سرور بارگذاری کارپوشه‌ها را می‌پذیرد؛ روی آن ماشین اکسلی نیست و هرگز نخواهد بود؛ و یک روز صبح مشتری یک .xlsx کاملاً عادی بارگذاری می‌کند که خواننده ZIP آن را رد می‌کند، چون اصلاً ZIP نیست. مشتری آن را با رمز عبور ذخیره کرده است. از آن لحظه، بارکننده شما یا [MS-OFFCRYPTO] را می‌فهمد یا فایل را به کاربری برمی‌گرداند که از دید خودش هیچ کار غیرعادی نکرده است

رمزگذاری Agile در یک فایل اکسل چیست؟

رمزگذاری Agile طرح محافظت با رمز عبوری است که در [MS-OFFCRYPTO] §2.3.4.10 تا §2.3.4.15 تعریف شده، و همان چیزی است که اکسل ۲۰۱۰ و بالاتر هر بار که کارپوشه‌ای با رمز عبور ذخیره شود می‌نویسند. فایل رمزگذاری‌شده دیگر یک بسته ZIP نیست. یک ظرف OLE Compound File Binary (CFB) است که دو جریان را در خود دارد: EncryptionInfo که شرح می‌دهد رمزگذاری چگونه انجام شده، و EncryptedPackage که همان ZIP واقعی .xlsx است، رمزگذاری‌شده به‌صورت یک بلاب مبهم. امضای CFB (D0 CF 11 E0 A1 B1 1A E1) همان امضای جادویی است که فایل‌های قدیمی BIFF .xls حمل می‌کنند، و به همین دلیل یک فایل تغییرنام‌یافته یا رمزگذاری‌شده را نمی‌توان تنها با پسوند دسته‌بندی کرد

آناتومی کارپوشه رمزگذاری‌شده Agile که HotXLS در دلفی باز می‌کند: یک ظرف CFB شامل جریان توصیف‌گر EncryptionInfo و جریان EncryptedPackage از بخش‌های AES-CBC
HotXLS یک کارپوشه رمزگذاری‌شده Agile را به‌صورت ظرف CFB می‌خواند، با جریان خودتوصیف EncryptionInfo و بسته ZIP اصلی که به‌صورت جریان مبهم EncryptedPackage رمزگذاری شده

آنچه Agile را از پیشینیانش متمایز می‌کند این است که EncryptionInfo خودتوصیف است. پس از یک پیشوند نسخه ۸ بایتی، با نسخه اصلی و فرعی هر دو برابر ۴، جریان یک توصیف‌گر XML با کدگذاری UTF-8 است. عنصر keyData رمز (AES)، حالت زنجیره‌سازی (ChainingModeCBC)، هش (SHA512)، طول کلید بر حسب بیت، اندازه بلوک و یک نمک Base64 را اعلام می‌کند. عنصر keyEncryptor مربوط به رمز عبور، نمک خود، مقدار spinCount و سه بار داده Base64 را حمل می‌کند: encryptedVerifierHashInput، encryptedVerifierHashValue و encryptedKeyValue. اکسل AES-256 را با شمارش چرخش ۱۰۰٬۰۰۰ می‌نویسد، ولی توصیف‌گر اجازه دارد AES-128 یا AES-192 را اعلام کند، و HotXLS به هر چه keyBits می‌گوید احترام می‌گذارد به‌جای آنکه ۲۵۶ را فرض بگیرد

یک نقطه ورود برای کارپوشه‌های ساده، Standard و Agile

TXLSXWorkbook.OpenEncrypted هر سه حالتی را که یک فراخواننده ممکن است با آن روبه‌رو شود مدیریت می‌کند، یعنی ZIP ساده، رمزگذاری‌شده Standard و رمزگذاری‌شده Agile، پس مدیریت‌کننده‌های بارگذاری لازم نیست پیش از بارکردن، فایل‌ها را دسته‌بندی کنند. متد ابتدا فایل را بو می‌کشد: اگر امضای CFB نباشد، کار را به مسیر عادی Open واگذار می‌کند و رمز عبور به‌سادگی نادیده گرفته می‌شود. اگر فایل یک ظرف CFB باشد، ابتدا رمزگذاری استاندارد ECMA-376 را امتحان می‌کند و هنگامی که امضای نسخه EncryptionInfo همان 4.4 مربوط به Agile باشد، به خط لوله Agile ارجاع می‌دهد. مقدار بازگشتی در صورت موفقیت 1 است، همان قراردادی که Open دارد

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // برای فایل‌های .xlsx ساده، رمزگذاری‌شده Standard و
    // رمزگذاری‌شده Agile به یک اندازه کار می‌کند
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

عقب‌نشینی برای ورودی رمزگذاری‌نشده بیش از آنچه به نظر می‌رسد اهمیت دارد. یک واردکننده دسته‌ای که همیشه OpenEncrypted را فرا می‌خواند به هیچ انشعابی در محل فراخوانی نیاز ندارد: فایل‌هایی که هرگز محافظت نشده‌اند دقیقاً مثل قبل بار می‌شوند، و فایل‌هایی که رمزگذاری‌شده می‌رسند در جا رمزگشایی و سپس به‌صورت یک جریان در حافظه به بارکننده معمولی ZIP داده می‌شوند. تنها یک مسیر کد برای آزمودن هست، نه سه تا

چگونه یک رمز عبور به کلید AES تبدیل می‌شود؟

رمزگذاری Agile هرگز مستقیماً از رمز عبور استفاده نمی‌کند. HotXLS نخست یک هش تکراری محاسبه می‌کند: خلاصه اولیه، SHA-512 روی نمک رمز عبور به‌علاوه بایت‌های UTF-16LE رمز عبور است، و سپس خلاصه به تعداد spinCount بار دوباره هش می‌شود و هر دور، شمارنده تکرار ۳۲ بیتی little-endian را پیش از خلاصه قبلی می‌چسباند. با شمارش چرخش پیش‌فرض اکسل یعنی ۱۰۰٬۰۰۰، این می‌شود صد هزار فراخوانی سریالی SHA-512 برای هر تلاش رمز عبور، و دقیقاً همین هدف کار است. شمارش چرخش یک محدودکننده حمله جست‌وجوی فراگیر است: برای فراخواننده مشروع یک بار چند میلی‌ثانیه هزینه دارد و برای مهاجم دیکشنری، همان چند میلی‌ثانیه را برای تک‌تک حدس‌ها

چگونه HotXLS کلیدهای رمزگذاری Agile را در دلفی استخراج می‌کند: رمز عبور از ۱۰۰٬۰۰۰ دور SHA-512 می‌گذرد، سپس سه کلید بلوکی ثابت ۸ بایتی کلیدهای راستی‌آزما و بسته را می‌سازند
رمز عبور هرگز مستقیماً چیزی را رمزگشایی نمی‌کند: یک زنجیره چرخشی SHA-512 سه هش کلید بلوکی را تغذیه می‌کند که کلیدهای راستی‌آزما را می‌سازند و کلید تصادفی بسته را باز می‌کنند
// هش تکراری رمز عبور طبق [MS-OFFCRYPTO]:
//   H(0) = SHA-512(salt + UTF-16LE(password))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1))، تکرارشده به تعداد spinCount بار
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);            // شمارنده تکرار، little-endian
    Move(Result[0], buf[4], 64);   // خلاصه پیشین
    Result := XlsSHA512(buf);
  end;
end;

هش چرخش‌یافته هنوز کلید نیست. سه کلید متمایز از آن استخراج می‌شوند، با یک بار هش‌کردن دوباره به‌همراه یک کلید بلوکی ثابت ۸ بایتی که به انتهایش چسبانده می‌شود، یک ثابت برای هر هدف: 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 به طول کلید اعلام‌شده کوتاه می‌شود و، طبق [MS-OFFCRYPTO]، در حالت نظری‌ای که هش کوتاه‌تر از کلید باشد با بایت‌های 0x36 پر می‌شود. همین قاعده پرکردن با 0x36 وقتی هم اعمال می‌شود که نمک رمز عبور برای استفاده به‌عنوان بردار اولیه CBC تا اندازه بلوک گسترش می‌یابد

تأیید رمز عبور و تله کوتاه‌سازی saltSize

HotXLS پیش از آنکه به بسته دست بزند، رمز عبور را با جفت راستی‌آزمای موجود در توصیف‌گر تأیید می‌کند. encryptedVerifierHashInput را با کلید استخراجی اول رمزگشایی می‌کند، نتیجه را با SHA-512 هش می‌کند، encryptedVerifierHashValue را با کلید استخراجی دوم رمزگشایی می‌کند و دو خلاصه را بایت‌به‌بایت مقایسه می‌کند. عدم تطابق یعنی رمز عبور اشتباه است، که به‌عنوان یک نتیجه مجزا گزارش می‌شود نه یک کارپوشه درهم‌ریخته، و مهم‌تر آنکه یعنی بدنه بسته هرگز با کلید بد رمزگشایی نمی‌شود، پس هیچ سناریویی وجود ندارد که در آن رمز عبور اشتباه، داده خرابِ ظاهراً معقول تولید کند

اینجا یک جزئیات از مشخصات فنی هست که به‌راحتی اشتباه گرفته می‌شود. [MS-OFFCRYPTO] §2.3.4.13 راستی‌آزما را saltSize بایت داده تصادفی تعریف می‌کند، جایی که saltSize طول نمک رمزنگار کلید است، نه اندازه بلوک رمز. چون متن رمز AES-CBC هم‌تراز با بلوک است، ورودی راستی‌آزمای رمزگشایی‌شده به‌صورت پرشده تا مضربی از ۱۶ بایت برمی‌گردد و باید پیش از هش‌شدن دوباره به saltSize کوتاه شود. اکسل همیشه saltSize را برابر blockSize می‌نویسد، هر دو ۱۶، پس پیاده‌سازی‌ای که از این کوتاه‌سازی صرف‌نظر کند همه آزمون‌ها را در برابر خروجی واقعی اکسل پاس می‌کند و بعد در نخستین فایل از تولیدکننده‌ای که طول نمک دیگری برگزیده شکست می‌خورد. HotXLS به طول نمک کوتاه می‌کند چون مشخصات دقیقاً همین را می‌گوید، و یکسان‌بودن این دو مقدار در عمل یک تصادف است نه یک قرارداد

EncryptedPackage چگونه رمزگشایی می‌شود؟

جریان EncryptedPackage با یک اندازه متن آشکار ۸ بایتی little-endian آغاز می‌شود و پس از آن متن رمز در بخش‌های ۴۰۹۶ بایتی می‌آید، و HotXLS آن را بخش‌به‌بخش با یک IV تازه برای هر بخش رمزگشایی می‌کند. خود کلید بسته از رمز عبور استخراج نمی‌شود: یک کلید میانی تصادفی است که نویسنده آن را درون encryptedKeyValue رمزگذاری کرده، و HotXLS آن را با کلید استخراجی سوم باز می‌کند و به طول کلید اعلام‌شده در keyData کوتاه می‌کند. مقدار IV هر بخش، SHA-512 روی نمک keyData به‌علاوه شاخص ۳۲ بیتی little-endian آن بخش است که تا اندازه بلوک کوتاه شده. این ساختار یعنی هر بخش ۴۰۹۶ بایتی می‌تواند مستقل رمزگشایی شود، که در اصل همان چیزی است که این قالب را برای دسترسی تصادفی مناسب می‌کند، هرچند HotXLS کل بسته را در حافظه رمزگشایی می‌کند و بایت‌های ZIP حاصل را به بارکننده معمولی XLSX خود می‌سپارد

HotXLS جریان EncryptedPackage را در دلفی بخش‌به‌بخش رمزگشایی می‌کند و IV هر بخش ۴۰۹۶ بایتی AES-CBC را از نمک keyData و شاخص بخش می‌سازد
هر بخش ۴۰۹۶ بایتی AES-CBC مستقل و با IV مخصوص خود از نمک و شاخص رمزگشایی می‌شود، و نتیجه تا اندازه متن آشکار اعلام‌شده کوتاه می‌شود

اندازه متن آشکار اعلام‌شده آخرین تکه کار را انجام می‌دهد. خروجی AES-CBC هم‌تراز با بلوک است، پس آخرین بخش تا ۱۵ بایت پرکننده حمل می‌کند که جزئی از سند نیست؛ بافر رمزگشایی‌شده به اندازه پیشوند کوتاه می‌شود و نتیجه دقیقاً همان ZIP فایل .xlsx است که اکسل رمزگذاری کرده بود. HotXLS پیشوند را پیش از رمزگشایی در برابر طول واقعی جریان اعتبارسنجی می‌کند، پس یک بارگذاری ناقص یا یک فیلد اندازه دست‌کاری‌شده به‌جای سرریز، تمیز شکست می‌خورد

گزارش خطا و مرزهای صادقانه

حالت‌های شکست عمداً از هم جدا نگه داشته شده‌اند. رمز عبور اشتباه، استثنایی با پیام صریح رمز عبور نادرست برمی‌انگیزد که از عدم تطابق راستی‌آزما می‌آید، پس یک رابط کاربری می‌تواند از کاربر بخواهد دوباره تلاش کند. یک ظرف CFB که توصیف‌گرش الگوریتم‌هایی بیرون از مجموعه پشتیبانی‌شده اعلام کند، یعنی هر چیزی جز AES با زنجیره‌سازی CBC و هش SHA-512 در یک توصیف‌گر Agile، یا ظرفی که نه Standard است و نه 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
      // هم برای رمز عبور اشتباه و هم برای طرح پشتیبانی‌نشده
      // برانگیخته می‌شود؛ E.Message می‌گوید کدام، پس آن را عیناً لاگ
      // کنید و تلاش دوباره رمز عبور را فقط برای حالت رمز نادرست بدهید
      RejectUpload(FileName, E.Message);
  end;
end;

بیان روشن مرزها ارزش دارد. HotXLS توصیف‌گرهای Agile را می‌خواند که AES در حالت CBC با SHA-512 را اعلام می‌کنند، که همان چیزی را پوشش می‌دهد که اکسل ۲۰۱۰ تا اکسل ۳۶۵ واقعاً می‌نویسند، در هر سه اندازه کلید. توصیف‌گرهایی که رمزها یا الگوریتم‌های هش دیگری اعلام کنند به‌جای حدس‌زدن رد می‌شوند، و رمزنگارهای کلید مبتنی بر گواهی اصلاً بررسی نمی‌شوند، تنها رمزنگار کلید مبتنی بر رمز عبور خوانده می‌شود. در سمت نوشتن، HotXLS در حال حاضر رمزگذاری استاندارد تولید می‌کند نه Agile، تمایزی که اگر ابزار پایین‌دستی طرح را بازرسی کند اهمیت پیدا می‌کند؛ جزئیات در مقاله نوشتن خروجی XLSX محافظت‌شده با AES آمده است

بارگذاری‌های محافظت‌شده با رمز عبور وقتی دیگر حالت خاص نیستند که بارکننده، رمزگذاری را بخشی از قالب فایل بداند نه استثنایی بر آن. نقطه ورود OpenEncrypted، استخراج کلید با چرخش SHA-512 و خط لوله بخش‌بندی‌شده AES-CBC که اینجا شرح داده شد، همگی به‌عنوان بخشی از HotXLS Delphi Excel Component عرضه می‌شوند، در کنار باقی موتور بومی خواندن و نوشتن XLS و XLSX آن برای دلفی و C++Builder