Technical Article

Почему Excel отклоняет зашифрованную книгу: ECB и RC4

Вы записываете книгу, шифруете ее с паролем, передаете файл коллеге, и тот открывает его в Excel. Excel запрашивает пароль. Коллега вводит его, и Excel принимает пароль. На первый взгляд шифрование кажется корректным. Но затем Excel выводит сообщение о том, что файл поврежден и не может быть открыт, либо открывает таблицу с некорректным содержимым ячеек. Пароль был верным, но файл все равно поврежден. Это наиболее запутанный сбой в шифровании документов Office, так как проверка пароля и защита данных выполняются в ходе двух разных операций, и успешность первой не гарантирует правильность второй

Обе описанные ошибки имели именно такую форму. В каждом случае блок проверки пароля успешно расшифровывался, а основное тело документа расшифровать не удавалось, из-за чего разработчики искали проблему в вычислении ключей, которой там не было. Настоящий дефект находился на этапе преобразования байтов пакета. Эти сбои независимы (один на пути AES, второй на пути RC4), но имеют схожие проявления, поэтому важно понимать причины частичного успеха расшифрования

Почему успешная проверка пароля не гарантирует целостность данных

Формат современного зашифрованного XLSX использует стандарт ECMA-376 Standard Encryption, при этом два зашифрованных элемента хранятся рядом. Первый представляет собой EncryptionVerifier (небольшой блок со случайным значением и его хэшем, зашифрованный вычисленным из пароля ключом). Второй элемент выражается в зашифрованном пакете EncryptedPackage (весь ZIP-контейнер книги, зашифрованный тем же ключом). Верификатор необходим для того, чтобы программа чтения могла проверить пароль перед расшифрованием мегабайтов данных. Достаточно расшифровать верификатор, рассчитать хэш случайного значения и сравнить его с сохраненным хэшем для проверки пароля

Ловушка заключается в том, что верификатор и пакет шифруются отдельными вызовами в разных буферах. Правильно вычисленный ключ расшифрует верификатор независимо от состояния пакета. Если формирование ключа выполнено верно, но преобразование пакета содержит ошибку, Excel подтвердит пароль, но выдаст ошибку на теле документа. Симптом в виде правильного пароля при поврежденном файле направляет отладку на путь проверки пароля, который работает корректно. Это же разделение применимо к устаревшему режиму RC4: сначала проверяется хэш верификатора, и сдвиг в расшифровании тела документа не влияет на эту проверку

Первая ошибка: AES в режиме ECB вместо CBC

Спецификация [MS-OFFCRYPTO] §2.3.4.15 определяет, что стандартное шифрование обрабатывает пакет с помощью AES в режиме Electronic Codebook. Каждый 16-байтный блок дополненного пакета шифруется независимо одним ключом. Сцепление блоков и вектор инициализации не используются. Это необычный выбор для современных стандартов, где режима ECB обычно избегают, но для совместимости необходимо строго следовать спецификации. Excel расшифровывает пакет в режиме ECB, поэтому средство записи должно использовать именно этот режим

Ошибка заключается в том, что пакет шифровался с помощью AES в режиме CBC с нулевым вектором инициализации. В режиме CBC первый блок открытого текста складывается по модулю 2 (XOR) с вектором инициализации перед шифрованием. При нулевом векторе эта операция ничего не меняет, поэтому первый блок шифрования CBC дает тот же результат, что и ECB. Но со второго блока режим CBC передает зашифрованный текст предыдущего блока в следующий, из-за чего все последующие блоки начинают отличаться от ECB

Посмотрим на структуру данных. Макет пакета содержит 8-байтовый префикс длины в самом начале, поэтому проверяемые Excel в первую очередь части файла находятся в первом блоке. Совпадение первого блока позволяет пройти начальную проверку, в то время как все последующие блоки расшифровываются как случайный шум. Исправление очевидно: шифровать каждый 16-байтный блок независимо в режиме ECB без связывания. В движке функция XlsEncryptStdPackage обходит дополненный буфер шагами по 16 байт и вызывает AESEncryptECB128Block для каждого из них (тот же примитив, что используется для верификатора). Комментарий в коде поясняет: CBC с нулевым вектором совпадает с ECB только для первого блока, а остальная часть пакета превратилась бы в нечитаемые данные, отклоняемые Excel

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serializes the workbook, then runs the
    // ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
    // package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Вторая ошибка: рассинхронизация смены ключа re-key в RC4

Путь для устаревших файлов .xls использует схему RC4 CryptoAPI со своими правилами. Спецификация [MS-OFFCRYPTO] §2.3.6 определяет, что шифр меняет ключ на каждой границе 1024-байтного блока. Поток делится на блоки по 1024 байта, для каждого блока вычисляется новый ключ RC4, а внутри блока поток ключа используется непрерывно от байта к байту. Должны соблюдаться два условия: смена ключа на границах блоков и использование потока ключа без пропусков внутри блока. Поскольку RC4 является потоковым шифром, его поток ключа представляет собой упорядоченную последовательность, где каждый байт зависит от предыдущих. Расшифрование является тем же XOR с той же последовательностью, поэтому записывающий и считывающий модули должны использовать одинаковые байты на тех же позициях

В этом и заключается сложность: потоковый шифр не имеет возможности повторной синхронизации. Пропуск всего одного байта смещает всю последовательность XOR для последующих данных, и эта ошибка не исправляется сама по себе, распространяясь до конца блока и переходя на следующие блоки. Ошибка заключалась именно в этом: счетчик блоков инициализировался значением -1, а процедура пропуска предполагала соответствие счетчика текущему блоку. Из-за этого выполнялась смена ключа и считывался лишний блок потока ключа размером 1024 байта, что приводило к сдвигу фазы расшифрования. Верификатор успешно проходил проверку, но все ячейки данных считывались как нечитаемый мусор

Исправленная логика реализована в классе TXLSDecrypterRC4. Методы Skip и Decrypt используют один цикл: ключ меняется только тогда, когда текущая позиция переходит границы нового блока (индекс блока равен позиции, деленной на REKEY_BLOCK_SIZE, то есть 1024), после чего данные обрабатываются до конца текущего блока. Функция MakeKey вызывается с правильным индексом блока, а позиция смещается на точное число обработанных байтов для сохранения синхронизации. Урок заключается в том, что один потерянный байт в потоковом шифре является критической ошибкой, разрушающей все последующие данные

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // CanReadEncrypted checks the Compound File (OLE2) signature so
    // you can branch before attempting a normal Open. OpenEncrypted
    // routes plain files to Open and handles the encrypted container.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // read cells here
  finally
    Book.Free;
  end;
end;

Совместимость с неизменяемой спецификацией требует побайтовой точности

Обе ошибки демонстрируют один принцип, важный при выборе проектных решений. Когда получателем данных является сторонняя программа, которую вы не можете изменить, режим шифрования и интервал смены ключа перестают быть деталями реализации, которые можно оптимизировать. Это жесткая часть контракта обмена данными. Excel будет расшифровывать данные в режиме ECB и менять ключ через каждые 1024 байта независимо от ваших предпочтений, и ваша задача состоит в создании байтов, пригодных для расшифрования по этим правилам. Более современный режим, другой вектор инициализации или иная инициализация счетчика превратятся в дефект, как только они разойдутся с требованиями Excel. Совместимость с неизменяемой спецификацией требует побайтовой точности

Поэтому проверка только верификатора является неполным тестом. Она подтверждает корректность формирования ключа из пароля, что необходимо, но недостаточно. Тест, который лишь открывает зашифрованный файл и проверяет ввод пароля, сообщит об успехе, даже если тело документа нечитаемо. Полноценное тестирование должно расшифровать пакет и сравнить полученные байты с оригиналу, либо выполнить цикл шифрования и расшифрования с проверкой ячеек данных. Верификатор подтверждает пароль, но шифрование подтверждается только целостностью тела документа

Поддерживаемый способ чтения и записи защищенных книг

Публичный интерфейс лаконичен. Чтобы записать защищенную паролем современную книгу, откройте или заполните объект TXLSXWorkbook и вызовите метод SaveAsEncrypted с именем файла и паролем: он сериализует книгу и выполнит стандартное шифрование, возвращая 1 в случае успеха. Для чтения вызовите метод CanReadEncrypted для проверки наличия шифрования OLE2, после чего выполните ветвление: OpenEncrypted обрабатывает зашифрованный путь и перенаправляет обычные файлы в Open. Работа с режимами и сменой ключей скрыта внутри движка, выполняя требования спецификации от вашего имени

var
  Book: TXLSXWorkbook;
  begin
    Book := TXLSXWorkbook.Create(nil);
    try
      Book.Open('quarterly.xlsx');
      Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
      // Reopen on the consumer side
      Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    finally
      Book.Free;
    end;
  end;

Структура зашифрованного вывода, поток EncryptionInfo, блоки верификатора и макет пакета подробно описаны в нашем руководстве по защищенному AES выводу XLSX. Вопросы блокировки на уровне листов и взаимодействия защиты с параметрами страницы и печати рассматриваются в статье о защите, настройке страниц и печати. Оба решения базируются на описанном здесь пути шифрования, поставляемом в составе компонента HotXLS для Delphi и C++Builder вместе с API для чтения, записи и рендеринга