Техническа статия

Четене на Agile-шифровани Excel файлове в Delphi с HotXLS

HotXLS чете Agile-шифровани Excel файлове — защитата с парола, която Excel 2010 и всяка по-късна версия прилагат по подразбиране — чрез едно-единствено извикване: TXLSXWorkbook.OpenEncrypted. Компонентът анализира XML дескриптора на шифроването, извежда ключове от паролата чрез SHA-512 хеш верига със spin count, проверява паролата срещу шифрования верификатор и след това дешифрира пакета на 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], или връща файла обратно на потребител, който от своя гледна точка не е направил нищо необичайно

Какво е Agile шифроване в Excel файл?

Agile шифроването е схемата за защита с парола, дефинирана в [MS-OFFCRYPTO] §2.3.4.10 до §2.3.4.15, и е това, което Excel 2010 и по-новите записват винаги, когато работна книга бъде записана с парола. Шифрованият файл вече не е ZIP пакет. Той е OLE Compound File Binary (CFB) контейнер, съдържащ два потока: EncryptionInfo, който описва как е извършено шифроването, и EncryptedPackage, който е истинският .xlsx ZIP, шифрован като непрозрачен blob. CFB сигнатурата (D0 CF 11 E0 A1 B1 1A E1) е същото магическо число, което носят наследените BIFF .xls файлове, поради което преименуван или шифрован файл не може да бъде класифициран само по разширение

Анатомия на Agile-шифрованата работна книга, която HotXLS отваря в Delphi: CFB контейнер, съдържащ поток с дескриптор EncryptionInfo и поток EncryptedPackage от AES-CBC сегменти
HotXLS чете Agile-шифрована работна книга като CFB контейнер със самоописващ се поток EncryptionInfo и .xlsx ZIP, шифрован като непрозрачен поток EncryptedPackage

Това, което отличава Agile от предшествениците му, е, че EncryptionInfo е самоописващ се. След 8-байтов префикс с версия, в който и главната, и второстепенната версия са 4, потокът е UTF-8 XML дескриптор. Елемент keyData декларира шифъра (AES), режима на верижно свързване (ChainingModeCBC), хеша (SHA512), дължината на ключа в битове, размера на блока и Base64 сол. Елемент keyEncryptor за парола носи собствена сол, spinCount и три Base64 полезни товара: encryptedVerifierHashInput, encryptedVerifierHashValue и encryptedKeyValue. Excel записва AES-256 със spin count 100 000, но на дескриптора е позволено да декларира AES-128 или AES-192, и HotXLS зачита каквото казва keyBits, вместо да предполага 256

Една входна точка за нешифровани, Standard и Agile работни книги

TXLSXWorkbook.OpenEncrypted обработва и трите състояния, на които извикващият може да попадне — обикновен ZIP, Standard-шифрован и Agile-шифрован — така че обработчиците на качвания не трябва да класифицират файловете, преди да ги заредят. Методът първо души файла: ако няма CFB сигнатура, той се отклонява към нормалния път Open и паролата просто се игнорира. Ако файлът е CFB контейнер, той първо опитва ECMA-376 Standard Encryption и, когато сигнатурата на версията в EncryptionInfo е Agile 4.4, препраща към 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 първо изчислява итериран хеш: началният digest е SHA-512 върху солта на паролата, конкатенирана с UTF-16LE байтовете на паролата, след което digest-ът се хешира отново spinCount пъти, като всеки кръг добавя отпред 32-битовия little-endian брояч на итерациите към предишния digest. При подразбирания за Excel spin count от 100 000 това са сто хиляди последователни SHA-512 извиквания на опит за парола — и точно това е смисълът. Spin count е спирачка срещу груба сила: струва на легитимния извикващ няколко милисекунди веднъж, а на атакуващия с речник — същите няколко милисекунди за всяко отделно предположение

Как HotXLS извежда ключове за Agile шифроване в Delphi: паролата минава през 100 000 SHA-512 кръга, после три фиксирани 8-байтови блокови ключа дават ключовете за верификатора и за пакета
Паролата никога не дешифрира нищо директно: SHA-512 spin-count верига захранва три хеша с блокови ключове, които произвеждат ключовете за верификатора и разопаковат случайния ключ на пакета
// [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);   // предишен 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 инициализиращ вектор

Проверка на паролата и капанът с отрязването до saltSize

HotXLS проверява паролата, преди да докосне пакета, използвайки двойката верификатори от дескриптора. Той дешифрира encryptedVerifierHashInput с първия изведен ключ, хешира резултата със SHA-512, дешифрира encryptedVerifierHashValue с втория изведен ключ и сравнява двата digest-а байт по байт. Несъвпадение означава, че паролата е грешна — докладвано като отделен резултат, а не като изкривена работна книга — и, което е решаващо, означава, че тялото на пакета никога не се дешифрира с лош ключ, така че няма сценарий, при който грешна парола произвежда правдоподобно изглеждащи повредени данни

Тук има подробност от спецификацията, която лесно се сбърква. [MS-OFFCRYPTO] §2.3.4.13 дефинира верификатора като saltSize байта случайни данни, където saltSize е дължината на солта на key encryptor-а, а не размерът на блока на шифъра. Тъй като 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-битовия little-endian индекс на сегмента, отрязан до размера на блока. Тази конструкция означава, че всеки 4096-байтов сегмент може да бъде дешифриран независимо, което по принцип прави формата дружелюбен и към произволен достъп, макар че HotXLS дешифрира целия пакет в паметта и подава получените ZIP байтове на нормалния си XLSX зареждащ модул

HotXLS дешифрира потока EncryptedPackage в Delphi сегмент по сегмент, извеждайки IV на всеки 4096-байтов AES-CBC сегмент от солта в keyData и индекса на сегмента
Всеки 4096-байтов AES-CBC сегмент се дешифрира независимо под собствен IV от сол и индекс, а резултатът се отрязва до декларирания размер на открития текст

Декларираният размер на открития текст върши последната част от работата. AES-CBC изходът е подравнен по блокове, така че последният сегмент носи до 15 байта допълване, които не са част от документа; дешифрираният буфер се отрязва до префикса с размера, а резултатът е точно .xlsx ZIP-ът, който Excel е шифровал. 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, което покрива това, което Excel 2010 до Excel 365 действително записват, и в трите размера на ключа. Дескриптори, деклариращи други шифри или хеш алгоритми, се отхвърлят, вместо да се гадае по тях, а базираните на сертификати key encryptor-и не се консултират — само key encryptor-ът за парола. От страната на записа HotXLS понастоящем произвежда Standard Encryption, а не Agile — разлика, която има значение, ако инструментите надолу по веригата инспектират схемата; подробностите са в статията за записване на AES-защитен XLSX изход

Защитените с парола качвания престават да бъдат специален случай, щом зареждащият модул третира шифроването като част от файловия формат, а не като изключение от него. Входната точка OpenEncrypted, SHA-512 spin-count извеждането и сегментираният AES-CBC конвейер, описани тук, се доставят като част от HotXLS Delphi Excel компонента, редом с останалата част от неговия нативен двигател за четене и запис на XLS и XLSX за Delphi и C++Builder