Вы записываете книгу, шифруете ее с паролем, передаете файл коллеге, и тот открывает его в 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) с IV перед шифрованием. Когда IV нулевой, эта операция ничего не меняет, поэтому первый блок CBC с нулевым IV даёт ровно тот же шифртекст, что и ECB. Со второго блока CBC подмешивает предыдущий шифртекст в следующий, так что каждый блок после первого расходится с ECB
Теперь наложим это на структуру. Макет пакета ставит 8-байтовый little-endian префикс длины в самое начало, так что части файла, которые Excel проверяет раньше всего, сидят в первом-втором блоке. Первый блок, случайно совпавший, означает, что самая ранняя проверка проходит, а все последующие блоки расшифровываются в шум. Лекарство становится очевидным, как только назван режим: шифровать каждый 16-байтный блок в ECB и не связывать. В движке XlsEncryptStdPackage обходит дополненный буфер шагами по 16 байт и вызывает AESEncryptECB128Block для каждого из них — тот же примитив, что уже используется для блоков верификатора. Рядом с циклом в исходнике стоит комментарий, формулирующий правило прямо: CBC с нулевым IV совпадает с ECB только для первого блока, поэтому остальная часть пакета расшифровалась бы в мусор, и Excel её отклонил
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('report.xlsx');
// SaveAsEncrypted сериализует книгу, а затем запускает
// конвейер стандартного шифрования ECMA-376: AES-128 ECB над
// пакетом согласно [MS-OFFCRYPTO] 2.3.4.15. Возвращает 1 при успехе.
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 не с тем байтом ключа, и ошибка никогда не исправляется сама: она каскадом идёт до конца блока, а стоит сдвинуться текущей позиции — и в каждый блок после. Малая ошибка в байтовой бухгалтерии делает ровно это. Представьте счётчик блоков, стартующий с сигнального значения «минус единица», и процедуру пропуска, которая предполагает, что счётчик уже совпадает с текущим блоком. Стартовав с сигнального значения, она делает re-key и прогоняет полные 1024 байта потока ключей, которые никогда не должны были быть потреблены, и заодно угоняет оставшийся счётчик в минус. С этой точки расшифровщик съехал на целый блок. Верификатор, проверяемый до всего этого, по-прежнему проходит, поэтому пароль выглядит верным, пока каждая ячейка данных не выходит мусором
В HotXLS логика живёт в TXLSDecrypterRC4. Skip и Decrypt делят один цикл: re-key только когда текущая позиция переходит в новый блок, где индекс блока — позиция, делённая на REKEY_BLOCK_SIZE (1024), затем потребление до остатка текущего блока и не дальше. MakeKey вызывается с индексом блока — никогда со stale- или сигнальным индексом, а позиция сдвигается на точное число обработанных байтов, чтобы Skip и Decrypt держали фазу с записывающей стороной. Урок сидит в самой мелкой единице: один сожжённый байт в потоковом шифре — не маленькая ошибка, это полная потеря всего ниже по течению
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
// Устаревший .xls, защищённый RC4 или RC4 CryptoAPI, расшифровывается
// внутри Open: передайте пароль, и потоковый расшифровщик держит темп
// re-key выровненным с записывающей стороной.
Book.Open('legacy.xls', 'S3cret!');
// здесь читаем ячейки
finally
Book.Free;
end;
end;
Совместимость с неизменяемой спецификацией требует побайтовой точности
Обе ловушки сводятся к одному корневому принципу, и его стоит проговорить отдельно, потому что он меняет вес проектных решений. Когда потребителем вашего вывода является фиксированная внешняя программа, которую вы не можете менять, режим шифра и темп re-key — не детали реализации, которые можно оптимизировать или упростить. Это часть проводного контракта. Excel будет расшифровывать в ECB и менять ключ на границах 1024 байтов независимо от того, нравятся вам эти решения или нет, и ваша единственная задача — выдать байты, которые расшифровываются в оригинал по этой точной процедуре. Более современный режим, безобидный на вид IV, счётчик, стартующий «где естественно»; любое из этих решений становится дефектом в тот момент, как разошлось с тем, чего ожидает читающая сторона. Совместимость с замороженной спецификацией не бывает приблизительной. Либо побайтовая точность, либо это сломано
Поэтому проверка только верификатора является неполным тестом. Она подтверждает корректность формирования ключа из пароля, что необходимо, но недостаточно. Тест, который лишь открывает зашифрованный файл и проверяет ввод пароля, сообщит об успехе, даже если тело документа нечитаемо. Полноценное тестирование должно расшифровать пакет и сравнить полученные байты с оригиналу, либо выполнить цикл шифрования и расшифрования с проверкой ячеек данных. Верификатор подтверждает пароль, но шифрование подтверждается только целостностью тела документа
Поддерживаемый способ чтения и записи защищенных книг
Публичная поверхность компактна. Чтобы записать защищённую паролем современную книгу, заполните или откройте TXLSXWorkbook и вызовите SaveAsEncrypted с именем файла и паролем; он сериализует книгу и запускает описанный выше конвейер Standard Encryption, возвращая 1 при успехе. SaveAsEncryptedAgile принимает те же аргументы и записывает схему Agile, которую Excel 2010 и новее используют по умолчанию, с выведением ключа SHA-512 и шифрованием пакета AES-256. Для чтения вызовите CanReadEncrypted, чтобы проверить, является ли файл контейнером Compound File, затем ветвь: OpenEncrypted расшифровывает пакеты и Standard, и Agile, а для обычных файлов делает fallback на Open, и бросает EXlsxEncryptionNotImplemented, когда пароль неверный или контейнер использует другую схему. Устаревший .xls — тоже Compound File, поэтому он идёт в TXLSWorkbook.Open с паролем. Обработка режимов и цикл re-key, описанные выше, сидят под этими вызовами; вы подаёте пароль и имя файла, а движок соответствует спецификации за вас
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly.xlsx');
Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
// Повторное открытие на стороне потребителя
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
Структура зашифрованного вывода, поток EncryptionInfo, блоки верификатора и макет пакета подробно описаны в нашем руководстве по защищенному AES выводу XLSX. Вопросы блокировки на уровне листов и взаимодействия защиты с параметрами страницы и печати рассматриваются в статье о защите, настройке страниц и печати. Оба решения базируются на описанном здесь пути шифрования, поставляемом в составе компонента HotXLS для Delphi и C++Builder вместе с API для чтения, записи и рендеринга