HotXLS читає зашифровані за допомогою Agile файли Excel, тобто захист паролем, який Excel 2010 та всі пізніші версії застосовують за замовчуванням, за допомогою одного виклику: TXLSXWorkbook.OpenEncrypted. Компонент аналізує XML-дескриптор шифрування, отримує ключі з пароля за допомогою ланцюжка хешів SHA-512 зі лічильником обертань, перевіряє пароль щодо зашифрованого верифікатора, а потім дешифрує пакет сегментами AES-CBC по 4096 байтів. При цьому не використовуються інсталяція Excel, COM або зовнішні криптографічні DLL
Ця стаття присвячена саме стороні читання шифрування Agile. Дві суміжні проблеми розглядаються у власних статтях: взаємодія із застарілими схемами RC4 та XOR всередині старих файлів BIFF .xls описана у статті про взаємодію з ECB та RC4, а створення захищених паролем робочих книг із шифруванням ECMA-376 Standard Encryption опис якого міститься у статті про захищений за допомогою AES вивід XLSX. У цьому ж випадку файл уже існує, його зашифрував хтось інший, і ваше завдання — відкрити його
Сценарій, який змушує вирішувати цю проблему, знайомий кожному, хто керує конвеєром обробки документів. Служба імпорту на стороні сервера приймає завантаження робочих книг; на машині немає і ніколи не буде Excel; і одного разу вранці користувач завантажує абсолютно звичайний файл .xlsx, який зчитувач ZIP відхиляє, оскільки це взагалі не ZIP. Користувач зберіг його з паролем. З цього моменту ваш завантажувач або розуміє [MS-OFFCRYPTO], або повертає файл користувачеві, який, з його точки зору, не зробив нічого незвичайного
What is Agile encryption in an Excel file?
Шифрування Agile — це схема захисту паролем, визначена в [MS-OFFCRYPTO] §2.3.4.10–§2.3.4.15, і саме її пишуть Excel 2010 та пізніші версії щоразу, коли робоча книга зберігається з паролем. Зашифрований файл більше не є пакетом ZIP. Це контейнер OLE Compound File Binary (CFB), що містить два потоки: EncryptionInfo, який описує, як було виконано шифрування, та EncryptedPackage, який є справжнім ZIP-файлом .xlsx, зашифрованим як непрозорий об'єкт. Підпис 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
One entry point for plaintext, Standard, and Agile workbooks
Метод 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;
Можливість роботи з незашифрованими вхідними даними має більше значення, ніж здається. Пакетний імпортер, який завжди викликає OpenEncrypted, не потребує розгалуження в місці виклику: файли, які ніколи не були захищені, завантажуються так само, як і раніше, а файли, що надходять зашифрованими, розшифровуються на місці, а потім передаються звичайному завантажувачу ZIP як потік у пам'яті. Потрібно тестувати один шлях коду, а не три
How does a password become an AES key?
Шифрування Agile ніколи не використовує пароль безпосередньо. HotXLS спочатку обчислює ітераційний хеш: початковий дайджест — це SHA-512 по солі пароля, об'єднаній з байтами UTF-16LE пароля, а потім дайджест повторно хешується spinCount разів, причому на кожному раунді до попереднього дайджесту додається 32-бітний лічильник ітерацій у форматі little-endian. При стандартному для Excel лічильнику обертань 100 000 це означає сто тисяч послідовних викликів SHA-512 на кожну спробу введення пароля, і в цьому полягає весь сенс. Лічильник обертань є обмежувачем підбору пароля: для законного викликача це коштує кілька мілісекунд один раз, а для зловмисника, що підбирає пароль за словником, — ті самі кілька мілісекунд на кожну спробу
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 урізається до оголошеної довжини ключа і, відповідно до [MS-OFFCRYPTO], доповнюється байтами 0x36 у теоретичному випадку, коли хеш коротший за ключ. Те саме правило доповнення 0x36 застосовується, коли сіль пароля розширюється до розміру блоку для використання як вектора ініціалізації CBC
Password verification and the saltSize truncation trap
HotXLS перевіряє пароль перед тим, як торкатися пакета, використовуючи пару верифікаторів із дескриптора. Він дешифрує encryptedVerifierHashInput за допомогою першого отриманого ключа, хешує результат за допомогою SHA-512, дешифрує encryptedVerifierHashValue за допомогою другого отриманого ключа і порівнює два дайджести байт за байтом. Невідповідність означає, що пароль є неправильним, про що повідомляється як про окремий результат, а не як про пошкоджену робочу книгу, і головне — тіло пакета ніколи не дешифрується неправильним ключем, тому немає сценарію, за якого неправильний пароль створював би правдоподібні на вигляд пошкоджені дані
Тут є деталь специфікації, яку легко зрозуміти неправильно. Документ [MS-OFFCRYPTO] §2.3.4.13 визначає верифікатор як saltSize байтів випадкових даних, де saltSize — це довжина солі шифратора ключа, а не розмір блоку шифру. Оскільки вихідні дані AES-CBC вирівнюються по блоках, дешифровані вхідні дані верифікатора повертаються доповненими до кратного 16 байтам значення, і їх необхідно урізати до saltSize перед хешуванням. Excel завжди пише saltSize рівним blockSize (обидва 16), тому реалізація, яка пропускає урізання, проходить кожен тест на реальному виводі Excel, а потім зазнає невдачі на першому ж файлі від генератора, який вибрав іншу довжину солі. HotXLS виконує урізання до довжини солі, оскільки саме це вимагає специфікація, а збіг двох значень на практиці є випадковістю, а не правилом
How is the EncryptedPackage decrypted?
Потік EncryptedPackage починається з 8-байтового префікса розміру відкритого тексту у форматі little-endian, за яким слідує шифротекст сегментами по 4096 байтів, і HotXLS дешифрує його сегмент за сегментом із новим IV на кожен сегмент. Сам ключ пакета не залежить від пароля: це випадковий проміжний ключ, який записувач зашифрував у encryptedKeyValue, і HotXLS розгортає його за допомогою третього отриманого ключа, урізаючи до довжини ключа, оголошеної в keyData. IV кожного сегмента є SHA-512 по солі keyData, об'єднаній з 32-бітним лічильником сегментів у форматі little-endian, урізаним до розміру блоку. Така конструкція означає, що будь-який сегмент розміром 4096 байтів може бути дешифрований незалежно, що в принципі робить формат зручним для довільного доступу, хоча HotXLS дешифрує весь пакет у пам'ять і передає отримані байти ZIP звичайному завантажувачу XLSX
Оголошений розмір відкритого тексту виконує фінальну частину роботи. Вихідні дані AES-CBC вирівнюються по блоках, тому останній сегмент містить до 15 байтів доповнення, які не є частиною документа; дешифрований буфер урізається до префікса розміру, і в результаті виходить саме той ZIP-файл .xlsx, який зашифрував Excel. HotXLS перевіряє префікс щодо фактичної довжини потоку перед дешифруванням, тому незавершене завантаження або підроблене поле розміру призводять до чистого збою замість виходу за межі діапазону
Error reporting and honest boundaries
Режими відмов навмисно розділені. Неправильний пароль викликає виняток із чітким повідомленням про неправильний пароль, що запускається невідповідністю верифікатора, тому інтерфейс користувача може запропонувати повторну спробу. Контейнер 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
// 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 наразі створює Standard Encryption, а не Agile, і ця різниця є важливою, якщо наступні інструменти перевіряють схему; деталі наведені в статті про запис захищеного за допомогою AES вивого XLSX
Захищені паролем завантаження перестають бути особливим випадком, коли завантажувач розглядає шифрування як частину формату файлу, а не як виняток із нього. Точка входу OpenEncrypted, деривація лічильника обертань SHA-512 та сегментований конвеєр AES-CBC, описані тут, постачаються як частина компонента HotXLS Delphi Excel Component, разом із іншою частиною його рідного рушія читання та запису XLS та XLSX для Delphi та C++Builder