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

Чтение файлов Excel с шифрованием Agile в Delphi с помощью HotXLS

HotXLS считывает файлы Excel с шифрованием Agile (защита паролем, применяемая по умолчанию в Excel 2010 и всех последующих версиях) с помощью одного вызова: TXLSXWorkbook.OpenEncrypted. Компонент разбирает XML-дескриптор шифрования, выполняет деривацию ключей из пароля с помощью хэш-цепочки SHA-512 со счетчиком итераций, сверяет пароль с зашифрованным верификатором и затем расшифровывает пакет сегментами по 4096 байт с использованием AES-CBC. Никакой установки 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 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

Единая точка входа для незашифрованных книг, а также книг со стандартным шифрованием и шифрованием 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
    // Одинаково работает для обычных .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 раз. При этом на каждом шаге перед предыдущим дайджестом добавляется 32-битный счетчик итераций в формате little-endian. При значении spinCount в 100 000, используемом в Excel по умолчанию, для каждой попытки ввода пароля выполняется сто тысяч последовательных вызовов SHA-512, в чем и заключается смысл защиты. Счетчик итераций замедляет перебор паролей: для обычного пользователя проверка занимает доли секунды, тогда как для злоумышленника при переборе по словарю задержка суммируется для каждой попытки

// Итеративный хэш пароля согласно [MS-OFFCRYPTO]:
//   H(0) = SHA-512(соль + UTF-16LE(пароль))
//   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;

Полученный хэш всё еще не является ключом. На его основе создаются три различных ключа путем еще одного хэширования с добавлением фиксированного 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 применяется при расширении соли пароля до размера блока для использования в качестве вектора инициализации (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-битным индексом сегмента в формате little-endian, усеченный до размера блока. Такая структура позволяет расшифровывать любой сегмент размером 4096 байт независимо от остальных, что в теории делает формат удобным для произвольного доступа, хотя на практике HotXLS расшифровывает весь пакет в память и передает результирующие байты ZIP стандартному загрузчику XLSX

Объявленный размер исходного текста выполняет финальный шаг работы. Вывод AES-CBC выравнивается по границе блоков, поэтому последний сегмент содержит до 15 байт дополнения, не относящегося к документу. Расшифрованный буфер усекается до значения размера из префикса, в результате чего получается ZIP-архив .xlsx в том виде, в каком его зашифровал Excel. HotXLS проверяет префикс по реальной длине потока перед расшифровкой, благодаря чему загрузка обрезанного файла или изменение поля размера приводят к ошибке, а не к переполнению буфера

Отчеты об ошибках и реальные ограничения

Ошибки различного типа четко разделяются компонентом. Неправильный пароль вызывает исключение с соответствующим сообщением на основе несовпадения верификаторов, что позволяет интерфейсу предложить повторный ввод. Контейнер CFB, дескриптор которого указывает неподдерживаемые алгоритмы (любые отличные от AES в режиме CBC с хэшированием SHA-512), или неподдерживаемую версию контейнера, вызывает другое исключение. Такое разделение критично: предложение повторного ввода пароля при несовместимом формате файла вводит пользователя в заблуждение, а описание ошибки пароля как повреждения структуры мешает работе службы поддержки

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 для всех трех длин ключей. Дескрипторы, использующие другие шифры или алгоритмы хэширования, отклоняются, а шифраторы ключей на основе сертификатов не поддерживаются (допускается только авторизация по паролю). На стороне записи HotXLS в настоящее время генерирует стандартное шифрование (Standard Encryption), а не Agile. Это отличие имеет значение, если сторонние инструменты проверяют схему; подробности описаны в статье о записи файлов XLSX с защитой AES

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