HotXLS записывает конформный блок dataIntegrity в пакеты XLSX с шифрованием Agile и проверяет его при открытии. HMAC-SHA-512 покрывает весь поток EncryptedPackage целиком, включая его восьмибайтовый префикс StreamSize, и проверяется по шифротексту ещё до расшифровки хотя бы одного сегмента, поэтому неверный пароль или изменённый пакет обнаруживаются, а не расшифровываются в мусор
Шифрование без проверки целостности — это лишь половина ответа, а форматы файлов Office позволяют легко упустить этот пробел, потому что снаружи шифрование выглядит настолько основательным. Понимание того, что именно обещает каждый уровень, — вот что делает security-ревью коротким
Что на самом деле обещает зашифрованная книга?
Шифрование Agile, определённое в [MS-OFFCRYPTO], даёт конфиденциальность через AES в режиме CBC с ключом, производным от итерированного хеша пароля SHA-512. Конфиденциальность — это всё, что обещает эта конструкция. CBC не является аутентифицированным режимом: он ничего не говорит о том, является ли расшифровываемый вами шифротекст тем самым шифротекстом, который был записан
Практическое следствие конкретно. Переверните биты в зашифрованном пакете — и CBC с готовностью расшифрует их в другой открытый текст. Обычно где-то дальше по цепочке вы получите ошибку разбора ZIP, потому что повреждённый поток deflate редко выживает, но слово «обычно» здесь несёт большую нагрузку, а ошибка парсера ниже по цепочке — ужасное место, чтобы узнать о том, что файл был изменён. Элемент dataIntegrity существует именно для того, чтобы ответить на этот вопрос напрямую, ещё до расшифровки, с помощью MAC по точным байтам
Как проходит проверка и в каком порядке
Интересная часть — именно порядок. HotXLS выводит промежуточный ключ из пароля, расшифровывает зашифрованные HMAC-ключ и HMAC-значение из атрибутов dataIntegrity с помощью IV, производных от блочного ключа, вычисляет HMAC-SHA-512 по зашифрованному пакету в том виде, в каком он хранится, и сравнивает результат. Только после этого начинается расшифровка сегментов
Проверка MAC по шифротексту, а не по открытому тексту, — это стандартная дисциплина encrypt-then-MAC, и именно она делает проверку осмысленной: изменённый пакет отклоняется без того, чтобы хоть один контролируемый злоумышленником байт прошёл через путь расшифровки и распаковки. Оба сравнения на пути открытия — хеш верификатора пароля и значение HMAC — накапливают различия через XOR и OR по всему дайджесту, а не завершаются досрочно при первом несовпавшем байте, поэтому ни одно из них не выдаёт позицию байта через тайминг
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
// Работает для обычных, Standard-зашифрованных и Agile-зашифрованных файлов
if Book.OpenEncrypted('incoming.xlsx', PasswordFromVault) = 1 then
ProcessWorkbook(Book)
else
// Неверный пароль либо пакет, чей HMAC dataIntegrity не совпал
Quarantine('incoming.xlsx');
except
on E: Exception do
Quarantine('incoming.xlsx: ' + E.Message);
end;
Book.Free;
end;
На стороне записи в вашем коде ничего не меняется. SaveAsEncrypted сам формирует блок, а соли, входные данные верификатора и HMAC-ключ берутся из CryptGenRandom. Если этот вызов завершается ошибкой, HotXLS вызывает исключение, а не откатывается к более слабому источнику. Отказ в закрытую сторону для CSPRNG — это не паранойя; тихий откат к предсказуемому источнику случайности даёт файлы, которые выглядят зашифрованными, проходят любой функциональный тест и при этом абсолютно бесполезны
Почему файлы без этого блока всё равно открываются?
Потому что огромное количество Agile-зашифрованных книг в обращении были записаны генераторами, которые полностью опускают dataIntegrity, и их отклонение сломало бы куда больше легитимной работы, чем защитило бы. HotXLS считает проверку целостности присутствующей только тогда, когда оба атрибута — зашифрованный HMAC-ключ и зашифрованное HMAC-значение — присутствуют и корректно сформированы. В противном случае проверка пропускается, и файл открывается как прежде
Это решение о совместимости с последствием для безопасности, которое стоит явно назвать в вашей собственной модели угроз: отсутствие блока нельзя отличить от того, что его удалил злоумышленник, потому что сами атрибуты находятся вне того MAC, который они бы несли. Если вы контролируете обе стороны конвейера, относитесь к отсутствующему блоку как к сбою политики на уровне приложения. Если вы принимаете файлы из внешнего мира, относитесь к проверке как к тому, чем она и является, — ценным сигналом при наличии и полным отсутствием сигнала при отсутствии
Пароль на изменение — это соглашение, а не граница защиты
Классические книги XLS поддерживают отдельный механизм, который регулярно путают с шифрованием, — резервирование записи, то есть запрос Excel «пароль для изменения». HotXLS предоставляет его через SetModifyPassword, который принимает пароль, флаг рекомендации «только для чтения» и имя резервирующего пользователя, а состояние сообщает через IsWriteReserved. Передача пустого пароля снимает резервирование
Записывается пара записей WRITEPROT и FILESHARING, несущих флаг рекомендации «только для чтения», устаревший 16-битный хеш пароля и имя пользователя в виде строки Unicode BIFF8. Этот 16-битный хеш — это контрольная сумма, а не криптографический дайджест, и содержимое документа вообще не шифруется. Любой, кто откроет файл любым другим инструментом, прочитает всё. Реальная задача этой функции — координация: она сообщает следующему человеку, что кто-то считает этот файл своим для редактирования, — из той же категории, что и элементы управления на уровне листа, рассмотренные в статье о защите листов XLSX и опциях allow
var
Book: IXLSWorkbook; // с подсчётом ссылок через интерфейс: не вызывать Free
begin
Book := TXLSWorkbook.Create;
if Book.Open('shared-model.xls') = 1 then
begin
// Рекомендовать только для чтения, зарезервировано службой отчётов
Book.SetModifyPassword('edit-me', True, 'Reporting Service');
if Book.IsWriteReserved then
Book.SaveAs('shared-model.xls', xlExcel97);
end;
end;
Используйте оба уровня для того, в чём каждый из них хорош. Настоящая конфиденциальность обеспечивается SaveAsEncrypted с паролем, которым не владеет никто за пределами аудитории, — это даёт вывод AES-256, описанный в статье о защищённом AES выводе XLSX. Резервирование записи добавляется поверх, когда книга является артефактом совместного редактирования и вы хотите, чтобы Excel спрашивал подтверждение перед перезаписью кем-то другим
Что проверять на пути приёма недоверенных файлов
Проверка целостности защищает зашифрованную полезную нагрузку, а не контейнер вокруг неё. Файл XLSX — это ZIP-архив, и структура архива разбирается ещё до того, как срабатывает любая логика шифрования, поэтому валидация на уровне контейнера должна стоять первой в цепочке; конкретные режимы отказа рассмотрены в статье о валидации EOCD ZIP для недоверенных XLSX. После этого относитесь к сбою целостности и к неверному паролю как к одному и тому же операционному событию, потому что с вашей стороны они неотличимы по замыслу, и оба означают, что файлу нельзя доверять быть тем, чем его считает отправитель
Логируйте, какие файлы вообще несли блок dataIntegrity. На выборке из нескольких тысяч документов эта статистика расскажет вам что-то полезное об инструментарии ваших отправителей и превратит проверку одного файла в наблюдение на уровне всего парка, на основании которого можно действовать
HotXLS читает и записывает XLS, XLSX и ODS из Delphi и C++Builder без установки Excel, реализуя пути шифрования [MS-OFFCRYPTO] Standard и Agile на Pascal. API шифрования, защиты и работы с книгами задокументирован на странице HotXLS Delphi spreadsheet component