Net-Base Журнал

30.05.2026

AES-шифрование в Delphi: надёжный фрагмент исходного кода с IV, солью, заголовком и потоковой обработкой

Практичный фрагмент исходного кода Delphi для AES‑шифрования со случайной солью и IV, с чёткой структурой заголовка файла, PBKDF2‑выведением ключа и потоковой обработкой — включая типичные подводные камни при работе с устаревшими форматами, вопросы целостности и эксплуатации.

30.05.2026

От темы в журнале к проектной практике

Соответствующие страницы услуг и технологий к статье

При AES-шифровании Delphi на практике редко дело в «самом AES», чаще — в сопутствующих условиях: данные необходимо обрабатывать как поток (файлы, BLOBы, резервные копии), старые форматы должны оставаться читаемыми, а в эксплуатации требуется отлаживаемость (заголовок, версионирование) и безопасные настройки по умолчанию (Salt/IV — случайные, без повторного использования). Поэтому этот фрагмент исходного кода показывает не только «Encrypt/Decrypt», но и небольшой, надёжный формат с заголовком, версией, солью и IV — плюс PBKDF2 для вывода ключа и место, где уместно дополнить проверку целостности.

Почему «AES-шифрование строки» почти никогда не достаточно

В проприетарном корпоративном ПО шифрование обычно встречается в трёх местах: (1) конфигурация/секреты (например, учётные данные), (2) обменные/экспортные файлы и (3) данные в покое (например, архивы, контейнеры документов). Наивный подход «пароль → AES-ключ → строка внутрь/наружу» быстро даёт сбои:

  • Повторное использование IV: В режимах вроде CBC или GCM вектор инициализации (IV) должен быть уникален для каждой операции шифрования. Постоянный IV — это утечка, даже если пароль сильный.
  • Ключ из пароля без KDF: Использование пароля напрямую как ключа (или единоразовое хеширование) открывает возможность оффлайн-атак. KDF (Key Derivation Function), например PBKDF2, целенаправленно замедляет атакующих.
  • Отсутствие версии формата: Без заголовка/версии вы вряд ли сможете изменить число итераций, алгоритм или параметры позже, не сделав старые данные «бесхозными».
  • Отсутствие целостности: AES-CBC шифрует, но не предотвращает модификации. Без аутентификации (например, HMAC или AEAD как GCM) вы получите проблемы с подменой битов/паддингом и труднодиагностируемые ошибки.

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

Шифрование AES Delphi с заголовком, солью, IV и PBKDF2

Мы определяем простой контейнерный формат, который также пригоден для BLOBов в базе данных или в полезной нагрузке сообщений:

  • Magic: 4 байта, например NBAE (быстрая проверка «это наш формат?»)
  • Version: 1 байт (позволяет миграцию)
  • Параметры KDF: число итераций (4 байта)
  • Salt: 16 байт (случайно для каждого файла)
  • IV: 16 байт (случайно для каждого файла для AES-CBC)
  • Ciphertext: зашифрованные полезные данные (поддерживает стриминг)

Важно: Salt и IV не являются секретом. Они должны быть уникальны для каждой операции шифрования. Пароль остаётся секретом; из него выведённый ключ не сохраняется.

AES-шифрование Delphi в потоке: запись/чтение контейнера

Код умышленно написан как «чертёж»: чётко разделённые функции, проверяемые заголовки, никаких скрытых глобальных переменных. Для AES и PBKDF2 многие команды используют проверенную криптобиблиотеку (например, DEC). Фрагмент демонстрирует формат и паттерн стриминга; вызовы AES/PBKDF2 инкапсулированы так, чтобы вы могли при необходимости заменить их в зависимости от используемой библиотеки.

Delphi
unit Nb.AesContainer;

interface

uses
  System.SysUtils, System.Classes, System.NetEncoding;

type
  ENbCryptoError = class(Exception);

  TNbAesContainer = class
  public
    class procedure EncryptStreamToStream(const AIn: TStream; const AOut: TStream;
      const APassword: string; const AIterations: Cardinal = 200000);

    class procedure DecryptStreamToStream(const AIn: TStream; const AOut: TStream;
      const APassword: string);

    class function EncryptBytesToBase64(const APlain: TBytes; const APassword: string): string;
    class function DecryptBase64ToBytes(const ACipherB64: string; const APassword: string): TBytes;
  end;

implementation

const
  CMagic: array[0..3] of AnsiChar = ('N','B','A','E');
  CVersion: Byte = 1;
  CSaltLen = 16;
  CIvLen   = 16;

type
  TNbHeaderV1 = packed record
    Magic: array[0..3] of AnsiChar;
    Version: Byte;
    Iterations: Cardinal; // little endian
    Salt: array[0..CSaltLen-1] of Byte;
    IV: array[0..CIvLen-1] of Byte;
  end;

// --- Abhängigkeiten, die Sie je nach Crypto-Stack implementieren ---

procedure FillRandomBytes(var B: TBytes);
begin
  // Für Kryptozufall: OS-CSPRNG verwenden (Windows BCryptGenRandom,
  // Linux getrandom/urandom). Hier absichtlich als Platzhalter.
  raise ENbCryptoError.Create('FillRandomBytes: CSPRNG nicht angebunden');
end;

function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
  const AIterations, AKeyLen: Cardinal): TBytes;
begin
  // Implementierung z. B. mit DEC (PBKDF2) oder anderer Bibliothek.
  // Ergebnis: AKeyLen Bytes.
  raise ENbCryptoError.Create('PBKDF2_HMAC_SHA256: nicht angebunden');
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
  // Implementierung per Bibliothek:
  // - KeyLen = 32 Bytes
  // - IVLen  = 16 Bytes
  // - PKCS#7 Padding
  // Wichtig: Stream-orientiert verarbeiten, nicht alles in Memory.
  raise ENbCryptoError.Create('AES256_CBC_EncryptStream: nicht angebunden');
end;

procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
  raise ENbCryptoError.Create('AES256_CBC_DecryptStream: nicht angebunden');
end;

// --- Helper ---

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
  if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
    raise ENbCryptoError.Create('Header konnte nicht geschrieben werden');
end;

function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
  H: TNbHeaderV1;
begin
  if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
    raise ENbCryptoError.Create('Header unvollständig');

  if (H.Magic[0] <> CMagic[0]) or (H.Magic[1] <> CMagic[1]) or
     (H.Magic[2] <> CMagic[2]) or (H.Magic[3] <> CMagic[3]) then
    raise ENbCryptoError.Create('Kein gültiger Container (Magic stimmt nicht)');

  if H.Version <> CVersion then
    raise ENbCryptoError.CreateFmt('Unbekannte Container-Version: %d', [H.Version]);

  if (H.Iterations < 10000) or (H.Iterations > 5000000) then
    raise ENbCryptoError.Create('Iterationszahl außerhalb plausibler Grenzen');

  Result := H;
end;

class procedure TNbAesContainer.EncryptStreamToStream(const AIn, AOut: TStream;
  const APassword: string; const AIterations: Cardinal);
var
  H: TNbHeaderV1;
  Salt, IV, Key: TBytes;
begin
  if APassword = '' then
    raise ENbCryptoError.Create('Passwort darf nicht leer sein');

  // Salt/IV erzeugen
  SetLength(Salt, CSaltLen);
  SetLength(IV, CIvLen);
  FillRandomBytes(Salt);
  FillRandomBytes(IV);

  // Header befüllen
  Move(CMagic[0], H.Magic[0], Length(CMagic));
  H.Version := CVersion;
  H.Iterations := AIterations;
  Move(Salt[0], H.Salt[0], CSaltLen);
  Move(IV[0], H.IV[0], CIvLen);

  WriteHeaderV1(AOut, H);

  // Key ableiten (32 Bytes für AES-256)
  Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);

  // Nutzdaten verschlüsseln (Ciphertext folgt direkt nach Header)
  AES256_CBC_EncryptStream(Key, IV, AIn, AOut);
end;

class procedure TNbAesContainer.DecryptStreamToStream(const AIn, AOut: TStream;
  const APassword: string);
var
  H: TNbHeaderV1;
  Salt, IV, Key: TBytes;
begin
  if APassword = '' then
    raise ENbCryptoError.Create('Passwort darf nicht leer sein');

  H := ReadHeaderV1(AIn);

  SetLength(Salt, CSaltLen);
  SetLength(IV, CIvLen);
  Move(H.Salt[0], Salt[0], CSaltLen);
  Move(H.IV[0], IV[0], CIvLen);

  Key := PBKDF2_HMAC_SHA256(APassword, Salt, H.Iterations, 32);

  // Entschlüsseln ab aktueller Stream-Position (nach Header)
  AES256_CBC_DecryptStream(Key, IV, AIn, AOut);
end;

class function TNbAesContainer.EncryptBytesToBase64(const APlain: TBytes;
  const APassword: string): string;
var
  InS, OutS: TBytesStream;
begin
  InS := TBytesStream.Create(APlain);
  try
    OutS := TBytesStream.Create;
    try
      EncryptStreamToStream(InS, OutS, APassword);
      Result := TNetEncoding.Base64.EncodeBytesToString(OutS.Bytes, 0, OutS.Size);
    finally
      OutS.Free;
    end;
  finally
    InS.Free;
  end;
end;

class function TNbAesContainer.DecryptBase64ToBytes(const ACipherB64,
  APassword: string): TBytes;
var
  Cipher: TBytes;
  InS, OutS: TBytesStream;
begin
  Cipher := TNetEncoding.Base64.DecodeStringToBytes(ACipherB64);
  InS := TBytesStream.Create(Cipher);
  try
    OutS := TBytesStream.Create;
    try
      DecryptStreamToStream(InS, OutS, APassword);
      Result := OutS.Bytes;
      SetLength(Result, OutS.Size);
    finally
      OutS.Free;
    end;
  finally
    InS.Free;
  end;
end;

end.

Назначение: Минимальный контейнер, подходящий для файлов и BLOB-объектов, включая версионирование и параметры KDF. Ограничения: Необходимо обеспечить реальную привязку к CSPRNG (криптографически стойкий источник случайных чисел из операционной системы) и надёжную реализацию AES/PBKDF2. Подводные камни: Не используйте «какой‑нибудь» Random (не Random()), не применяйте фиксированные IV, и при расшифровке предусмотрите однозначную обработку ошибок (неправильный пароль vs. повреждённые данные). Варианты: вместо CBC предпочтительнее AEAD (см. ниже), либо расширить заголовок идентификатором алгоритма и HMAC.

Целостность: почему AES-CBC в эксплуатации сам по себе рискован

AES-CBC всё ещё встречается во многих наследуемых контекстах и может работать, если вы дополнительно применяете механизм контроля целостности. Без контроля целостности злоумышленник может модифицировать шифротекст; даже при отсутствии активного атакующего ошибки передачи или повреждённые слои хранения приводят к трудно диагностируемым «padding»-ошибкам.

Практические варианты:

  • Encrypt-then-HMAC: После шифротекста записать HMAC (например HMAC-SHA-256) по Header+Ciphertext. При чтении сначала проверить HMAC, затем расшифровать. Для этого желательно вывести из PBKDF2 два ключа (например 64 байта: 32 для AES, 32 для HMAC), вместо повторного использования одного и того же ключа.
  • AES-GCM: AEAD-режим (Authenticated Encryption with Associated Data). Даёт шифротекст и аутентификационный тег. Поля заголовка можно аутентифицировать как «AAD», не шифруя их.

Если вам нужно остаться на CBC (например, ради совместимости), то Encrypt-then-HMAC — это надёжное дополнение. Для новых форматов имеет смысл использовать GCM, поскольку он обеспечивает аутентификацию «из коробки» и делает картины ошибок понятнее.

Особо важно: «криптографический случайный источник» и почему System.Hash не годится

Типичный наследуемый рефлекс в Delphi-проектах: «Мы просто берём SHA256 от метки времени + чего‑нибудь и получаем Random». Это ненадёжная основа. Для Salt и IV вам нужен CSPRNG (Cryptographically Secure Pseudo Random Number Generator) операционной системы. Под Windows это обычно BCrypt-API (CNG), под Linux — генератор ядра вроде getrandom() или /dev/urandom. Практическая разница в том, что CSPRNG сконструирован так, чтобы по наблюдаемым данным нельзя было предсказать последующие значения.

Архитектурный приём: инкапсулируйте это в небольшую unit «RandomProvider», которую можно мокать в тестах. Это решает сразу две задачи: воспроизводимые тесты (фиксированный seed в моке) и реальная безопасность в продакшене (OS-CSPRNG). Так вы предотвращаете ситуацию, когда в хотфиксе «быстро и просто» возвращают Random(), потому что это удобно.

Отладка и миграция наследия: версионирование — не роскошь

Заголовок нужен не только для «крипто‑аккуратности», но и для сопровождения:

  • Настройка числа итераций: Число итераций PBKDF2 меняется с годами. С полем в заголовке вы сможете повысить его позже, не делая старые данные нечитаемыми.
  • Смена формата: Версия 2 может, например, перейти на AES-GCM или добавить HMAC.
  • Диагностика в полевых условиях: Magic/Version позволяют быстро проверять в логах и инструментах без необходимости расшифровывать данные.

Практический совет: реализуйте небольшой «Inspector», который считывает только заголовок (Magic/Version/Iterations) и записывает в лог. Так вы проясните многие вопросы поддержки («Какая здесь версия?») без работы с паролями.

Чистая миграция: „Read old, write new“ вместо Big Bang

Если вы заменяете старый формат (напр., фиксированный IV, отсутствие KDF, Blowfish/3DES или самодельный XOR), в Delphi-проектах зарекомендовала себя следующая схема: при чтении распознавайте несколько форматов (Magic/Version или эвристика обратной совместимости), при записи генерируйте только новый формат. Дополнительно, при успешном расшифровании можно в фоне выполнить ре-шифрование («lazy migration»), если это вписывается в процесс. Это сокращает риск отката и избавляет от необходимости «одновременно перешифровать всё» в окне технического обслуживания.

Потоки и многопоточность: типичные узкие места в Delphi

Шифрование часто выполняется в рабочих потоках (например, при экспорте, при загрузке в портал клиента, при записи больших архивов). Два момента, которые в Delphi-проектах регулярно проявляются:

  • Позиции потока: перед шифрованием/расшифрованием задавайте чёткие контрактные положения: входной поток читается с текущей позиции, выходной поток записывается с текущей позиции. При повторном использовании потоков обязательно сознательно устанавливайте Position := 0.
  • Пики использования памяти: избегайте «всё в Терабайтах». Подход на основе потоков особенно важен для больших файлов. Если ваша криптобиблиотека принимает только байтовые массивы, имеет смысл вложить дополнительную работу в переход к stream-ориентированной реализации или построить буферизующий адаптер.

Если вы шифруете в сервисах (в Windows- или Linux-Services), дополнительно позаботьтесь о корректном логировании исключений: «неверный пароль», «заголовок повреждён», «Tag/HMAC недействителен» — это разные эксплуатационные случаи и их следует различать. Важно: сообщения об ошибках не должны быть чрезмерно подробными для внешних клиентов (никакого «Padding неверен в блоке 7» как API-ошибка), но внутри логов подробности допустимы.

Когда подход оправдан — и где он может сломаться

Оправдан, когда вы: (a) надёжно храните зашифрованные данные экспорта/импорта, (b) эксплуатируете параллельно разные версии программ, (c) обрабатываете данные потоками или (d) нуждаетесь в чистом крипто-интерфейсе для нескольких модулей (клиент/сервер/утилиты).

Не подходит, если вы пытаетесь решить с его помощью «всё подряд»: за транспорт отвечает TLS, а не самодельная оболочка вокруг AES. Для секретов (пароли, токены) чаще уместнее системный хранилище секретов ОС или Vault. И если нужна межъязыковая совместимость, заголовки, порядок байтов (endianness) и кодировки должны быть задокументированы точно (или используйте уже зарекомендовавший себя формат).

Вывод: AES в Delphi — это меньше алгоритм, больше инженерия

Главный выигрыш этого набора приёмов — не в том, что «AES работает», а в наличии эксплуатационно пригодного формата: случайный salt и IV, версионированный заголовок, параметры PBKDF2 в нагрузке и потоково-ориентированная обработка. Для новых форматов по возможности добавляйте целостность (AES-GCM или Encrypt-then-HMAC). Это превращает «мы что-то шифруем» в компонент, который в корпоративных цифровых решениях остаётся поддерживаемым и мигрируемым даже через годы.

Если вам нужно интегрировать такой контейнер в существующий Delphi-ландшафт или корректно мигрировать его из унаследованного формата, имеет смысл провести краткий проверочный обзор архитектуры (управление ключами, версии форматов, эксплуатация/логирование). Подробности при необходимости обсудим в разговоре:

В предметной области также важную роль играют Delphi Aes и Pbkdf2 Delphi, когда интеграции, потоки данных и дальнейшая разработка должны корректно взаимодействовать.

Обсудить проект или задачу по модернизации с Net-Base.

Следующий шаг

Если из темы вырастет реальный проект, архитектуру, текущую систему и эксплуатацию следует рассматривать совместно на ранних этапах.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

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

Поделиться записью

Поделиться этой записью напрямую

LinkedIn, X, XING, Facebook, WhatsApp и E-Mail доступны сразу. Для Instagram мы сразу подготовим ссылку и краткий текст.

Электронная почта

Instagram открывается в новой вкладке. Ссылка и короткий текст предварительно копируются в буфер обмена.