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

HotXLS Delphi Component: XLSX AES-protected output в Delphi

Excel предоставляет две вещи, каждую из которых называют «паролем», и только одна из них — это шифрование. Пароль открытия задаёт ключ настоящего шифра: без него файл вообще нельзя прочитать. Пароли защиты листа и книги не делают ничего подобного. Они устанавливают флаг, который согласившийся сотрудничать редактор обязуется уважать, а книга, несущая лишь этот флаг, — обычный читаемый zip-архив с данными в открытом виде. Выберите не тот вариант — и вы поставите зарплатную ведомость, которая выглядит запертой в Excel и читается в любом текстовом редакторе

Доказательство занимает десять секунд. Переименуйте защищённый .xlsx в .zip, откройте его в любом архиваторе и загляните в xl/worksheets/sheet1.xml. Если значения ячеек лежат там открытым текстом в UTF-8, файл не зашифрован, сколько бы запросов пароля ни выдавал Excel при попытке отредактировать ячейку. Этот разрыв годами живёт внутри команд, которые считают защиту листа конфиденциальностью, и обычно всплывает в тот день, когда проверка безопасности проделывает именно это переименование

HotXLS — нативная библиотека для электронных таблиц для Delphi и C++Builder, и она держит эти две возможности по разные стороны этой границы. Защита листа и книги — это ограничения на редактирование, опирающиеся на намеренно слабый устаревший хеш. SaveAsEncrypted создаёт AES-зашифрованный пакет, который не откроет ничто, кроме пароля. Разделы ниже разбирают, что именно записывает этот вызов, асимметрию, вокруг которой придётся строить архитектуру (HotXLS записывает зашифрованные файлы, но не может прочитать их обратно), и то, чем отличается более старый путь XLS

Диаграмма контраста: защита листа XLSX в Delphi, хранящая слабый хеш и оставляющая данные ячеек читаемыми в обычном zip, против SaveAsEncrypted в HotXLS, выводящего ключ AES-128 и пишущего контейнер шифрования OLE
Защита листа хранит слабый хеш и оставляет пакет читаемым zip. SaveAsEncrypted выводит ключ AES-128 и пишет OLE-контейнер, который ни один архиватор не сможет перечислить

Почему защита листа — это не шифрование

Методы Protect у листов и ProtectWorkbook у книги хранят хеш пароля из 4 шестнадцатеричных цифр. Это устаревший алгоритм, унаследованный OOXML и BIFF ещё от Excel девяностых, и документация формата никогда не утверждает, что он делает больше, чем предотвращает случайные правки. Пакет остаётся обычным читаемым zip-архивом: данные ячеек, формулы и общие строки — всё в открытом XML. Значение по умолчанию делает ситуацию хуже, а не лучше. Каждая ячейка начинает жизнь с Locked=True, поэтому вызов Protect без предварительного снятия блокировки с диапазона для ввода замораживает весь лист от редактирования, оставляя при этом каждое значение на виду

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

Что на самом деле записывает SaveAsEncrypted

Реализация следует ECMA-376 Standard Encryption, описанной в разделе 2.3.4 [MS-OFFCRYPTO]. Пароль проходит через 50 000 итераций SHA-1 для получения ключа AES-128. Блок-верификатор, зашифрованный AES-128 в режиме ECB, позволяет потребителю подтвердить пароль ещё до расшифровки чего-либо, а затем весь пакет книги шифруется AES-128 в режиме CBC. То, что попадает на диск, — это вообще не zip-архив. Это составной файл OLE, содержащий потоки EncryptionInfo, EncryptedPackage и DataSpaces, без каталога xl/, который мог бы перечислить архиватор, поэтому тест с переименованием теперь не выдаёт ничего читаемого. Excel 2007 и новее открывает его одним лишь паролем, и актуальный LibreOffice тоже умеет читать Standard Encryption

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  rc: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Payroll');
    Sheet.Cells[1, 1].Value := 'Employee';
    Sheet.Cells[1, 2].Value := 'Net pay';
    Sheet.Cells[2, 1].Value := 'A. Garcia';
    Sheet.Cells[2, 2].Value := 4815.16;

    rc := Book.SaveAsEncrypted('payroll-2026-06.xlsx', PasswordFromVault);
    if rc <> 1 then
      raise Exception.CreateFmt('Encrypted save failed (rc=%d)', [rc]);
  finally
    Book.Free;
  end;
end;

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

Есть и машинно проверяемый приёмочный тест, который почти ничего не стоит: вызовите CanReadEncrypted на только что записанном файле. Он возвращает true только тогда, когда результат действительно является контейнером шифрования, поэтому проверка этого условия после каждого зашифрованного сохранения ловит самую важную регрессию — код, который тихо откатился к обычному SaveAs, — в момент, когда это происходит, а не неделями позже во входящих у клиента. Последнее слово по-прежнему остаётся за ручным открытием в Excel с настоящим паролем во время релизного тестирования

Только запись по замыслу: обработка EXlsxEncryptionNotImplemented

Вот асимметрия, которая должна определять архитектуру вашего конвейера: HotXLS шифрует при сохранении, но не расшифровывает при открытии. OpenEncrypted выбрасывает EXlsxEncryptionNotImplemented, когда указывает на реально зашифрованный пакет; на обычной книге он просто проваливается в обычный Open. Сопутствующая проверка CanReadEncrypted дёшево обнаруживает контейнер шифрования OLE, так что код приёмки может направлять такие файлы, не вызывая исключение:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.CanReadEncrypted(FileName) then
    begin
      // Зашифрованный контейнер: HotXLS не может его расшифровать
      Writeln(FileName + ': needs manual decryption in Excel first');
      Exit;
    end;
    try
      Book.OpenEncrypted(FileName, '');   // обычные файлы проваливаются в Open
      Writeln(FileName + ': opened, ' + IntToStr(Book.Sheets.Count) + ' sheet(s)');
    except
      on EXlsxEncryptionNotImplemented do
        Writeln(FileName + ': encrypted - routed to manual queue');
    end;
  finally
    Book.Free;
  end;
end;

У этой асимметрии есть одно ясное архитектурное прочтение: шифруйте на краю доставки, последним шагом. Держите открытый эталонный экземпляр внутри границы доверия — в базе данных, хранилище документов или ресурсе с контролем доступа — и создавайте зашифрованную копию как финальный шаг перед тем, как файл покинет систему. Конвейер, архивирующий только зашифрованный вывод, сам себя запер от собственных данных, потому что ни один более поздний этап той же системы не сможет заново открыть эти файлы. Когда нижестоящему процессу HotXLS снова понадобится книга, отдавайте ему открытый эталон, а не артефакт доставки

Диаграмма конвейера вызова SaveAsEncrypted в HotXLS в Delphi: пароль хранилища прокручивается через 50 000 раундов SHA-1 в ключ AES-128, блок verifier ECB и шифрование пакета CBC, создавая составной файл OLE, проверяемый CanReadEncrypted
Один вызов SaveAsEncrypted превращает пароль хранилища в ключ AES-128 и составной OLE-файл. CanReadEncrypted даёт сохранению машинно-проверяемые ворота приёмки

AES-128 Standard Encryption и граница соответствия требованиям AES-256

Шифрование файлов Office существует в двух поколениях. Standard Encryption, то, что записывает HotXLS, использует AES-128 с выведением ключа через SHA-1. Agile Encryption появилось позже и переходит на AES-256 с SHA-512 и другим, описанным в XML контейнером ключа. Оба прозрачно открываются в Excel, и AES-128 всё ещё вычислительно надёжен для защиты файла на пути к клиенту

Архитектурная диаграмма для сервисов Delphi: мастер книги открытым текстом остаётся внутри границы доверия, SaveAsEncrypted в HotXLS выполняется последним шагом на краю доставки, плюс предупреждение не архивировать только зашифрованную копию
Шифруйте на краю доставки, последним, из открытого мастера, хранимого внутри границы доверия. Архивирование одной лишь зашифрованной копии запирает каждую следующую стадию вне её собственных данных

Разница перестаёт быть чисто академической в тот день, когда анкета по безопасности требует «шифрования файлов в состоянии покоя AES-256». Standard Encryption не дотягивает до этой планки, каким бы сильным ни был пароль, и ни один параметр SaveAsEncrypted не меняет алгоритм, который он выдаёт. Поэтому точно указывайте профиль в своей документации по безопасности: AES-128, ECMA-376 Standard Encryption, вывод ключа через SHA-1 за 50 000 итераций. Утверждение, которое переживает проверку, стоит больше, чем оптимистичное, которое рушится под аудитом

Устаревший путь XLS: RC4 наружу, RC4 и XOR обратно

У фасада BIFF форма противоположная. Его шифрование старше и слабее, но обратный цикл полон: что он записывает, то же самое он может прочитать обратно. Установка EncryptionPassword перед SaveAs создаёт RC4-зашифрованный .xls через механизм BIFF FilePass, а Open с параметром пароля читает все три устаревшие схемы: RC4, RC4 CryptoAPI и древнюю обфускацию XOR:

var
  Writer, Reader: IXLSWorkbook;   // интерфейсные ссылки: ручной Free не нужен
begin
  Writer := TXLSWorkbook.Create;
  Writer.Sheets.Add.Cells.Item[1, 1].Value := 'Confidential';
  Writer.EncryptionPassword := 'S3cret!';
  Writer.SaveAs('confidential.xls');

  Reader := TXLSWorkbook.Create;
  if Reader.Open('confidential.xls', 'S3cret!') > 0 then
    Writeln(Reader.Sheets[1].Cells.Item[1, 1].Value);  // нумерация элементов начинается с 1
end;

RC4 — устаревшая криптография, и ею никогда не следует защищать данные, важные сегодня; её единственная оставшаяся ценность — совместимость с системами, которые всё ещё обмениваются .xls. Однако сторона чтения окупается в задачах миграции. Устаревший файл, защищённый паролем, открывается через Open(FileName, Password), переносится в модель OOXML и заново защищается через путь AES — однонаправленное обновление, которое выполняется без единого участия Excel. Для доставок с большим объёмом зашифрованных файлов заметки о пропускной способности на стороне записи из нашей статьи о потоковой записи для серверных пакетных задач применимы к фазе построения содержимого, которая происходит до шифрования

Шифрование и защита — не соперники

Стоит расставить точки ещё в одном моменте, потому что он всплывает в тот же миг, когда кто-то читает предупреждение в начале этой страницы как «защита бесполезна». Это не так. Шифрование и защита отвечают на разные вопросы и аккуратно складываются друг с другом. Шифрование решает, кто может открыть файл; защита решает, что может изменить читатель, уже оказавшийся внутри. Доставка зарплатной ведомости вполне разумно может делать и то, и другое: зашифровать пакет так, чтобы его видел только владелец пароля, а затем заблокировать ячейки с формулами, чтобы получатель мог фильтровать и сортировать, но не мог тихо переписать вычисления. Ошибка — вовсе не добавление защиты. Ошибка — позволить её присутствию подменить собой шифрование, когда требованием была конфиденциальность

У стороны хранения нет страховочной сетки, и так задумано. Вывод ключа за 50 000 итераций существует, чтобы сделать подбор пароля дорогим, и ничто внутри файла не депонирует секрет. Утерянный пароль — это утерянные данные. Генерируйте, доставляйте и храните эти пароли с той же дисциплиной, что вы применяете к учётным данным баз данных, и шифрование выполнит свою часть работы

Настоящее шифрование файла в HotXLS — это один вызов. Дисциплина живёт во всём, что окружает этот вызов: хранение пароля, граница «только запись», которая не даёт HotXLS заново открыть собственный вывод, и утверждение об алгоритме, которое можно защитить на аудите. SaveAsEncrypted и устаревший обратный цикл поставляются вместе с HotXLS Delphi Component, работающим нативно в процессах Delphi и C++Builder без какой-либо автоматизации Excel на всём пути