Net-Base Журнал

30.05.2026

AES-шифрування в Delphi: надійний фрагмент вихідного коду з IV, сіллю, заголовком і потоковим шифруванням

Практичний Delphi-фрагмент вихідного коду для AES-шифрування з випадковим Salt і IV, чіткою структурою заголовка файлу, PBKDF2-деривацією ключа та потоковою обробкою — включно з типовими підводними каменями в legacy-форматах, питаннями цілісності та експлуатації.

30.05.2026

Від теми журналу до практики проєкту

Відповідні сторінки послуг і технічні сторінки до публікації

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

Чому «AES-шифрування рядка» майже ніколи не достатнє

У кастомному корпоративному ПЗ шифрування зазвичай з’являється в трьох місцях: (1) конфігурація/секрети (наприклад, облікові дані), (2) файли обміну/експорту та (3) дані у спокої (наприклад, архіви, контейнер документів). Наївний підхід «пароль → AES-ключ → рядок в/з» швидко дає збої:

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

Сутність цього матеріалу: невеликий контейнерний формат, який підтримує потокову обробку, має версії і уникатиме стандартних помилок.

AES Verschlüsselung Delphi з заголовком, Salt, IV та PBKDF2

Ми визначаємо простий контейнерний формат, який також можна використовувати в BLOBах бази даних або в payload повідомлень:

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

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

AES Verschlüsselung Delphi у потоці: запис/читання контейнера

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

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;

// — Залежності, які потрібно реалізувати залежно від крипто-стеку —

procedure FillRandomBytes(var B: TBytes);
begin
// Для криптографічного випадкового заповнення: використовувати системний CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Тут навмисно залишено як заповнювач.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG не реалізовано‘);
end;

function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Реалізація, напр., з використанням DEC (PBKDF2) або іншої бібліотеки.
// Результат: AKeyLen байтів.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: не реалізовано‘);
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Реалізація через бібліотеку:
// – KeyLen = 32 байти
// – IVLen = 16 байтів
// – PKCS#7 padding
// Важливо: обробляти у потоковому режимі, не завантажувати все в пам’ять.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: не реалізовано‘);
end;

procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: не реалізовано‘);
end;

// — Допоміжні функції —

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Не вдалося записати заголовок‘);
end;

function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Заголовок неповний‘);

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(‚Неприпустимий контейнер (magic не збігається)‘);

if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Невідома версія контейнера: %d‘, [H.Version]);

if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Кількість ітерацій поза допустимими межами‘);

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(‚Пароль не може бути порожнім‘);

// Генерація salt/IV
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);

// Заповнення заголовка
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);

// Виведення ключа (32 байти для AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);

// Шифрування корисних даних (шифротекст іде відразу після заголовка)
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(‚Пароль не може бути порожнім‘);

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);

// Розшифрування з поточної позиції потоку (після заголовка)
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 досі зустрічається у багатьох legacy-контекстах і може працювати, якщо ви додатково використовуєте механізм забезпечення цілісності. Без гарантії цілісності нападник може модифікувати шифротекст; навіть без активного нападника помилки при передачі або дефектні шари зберігання призводять до важко діагностованих помилок паддінгу.

Практичні варіанти:

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

Якщо ви змушені залишитися на CBC (наприклад через сумісність), Encrypt-then-HMAC — надійне доповнення. Для нових форматів AES-GCM виправдовує себе тим, що дає автентифікацію «за замовчуванням» і робить картини помилок зрозумілішими.

Неначе непропорційно важливо: «криптографічний випадок» і чому System.Hash не вистачає

Поширений legacy-рефлекс у 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().

Налагодження та миграція legacy: версіювання — це не розкіш

Хедер потрібен не лише для «крипто-красоти», а для підтримуваності:

  • Iteration Tuning: числа ітерацій PBKDF2 змінюються з роками. З полем у хедері ви зможете згодом підвищити їх, не роблячи старі дані нечитаємими.
  • Formatwechsel: версія 2 може, наприклад, перейти на AES-GCM або доповнити формат HMAC.
  • Diagnose im Feld: Magic/Version дозволяють оперативно перевіряти логи і інструменти без розшифрування даних.

Практична порада: Реалізуйте невеликий «інспектор», який читає лише заголовок (Magic/Version/Iterations) і записує його в лог. Це дозволить вирішити багато випадків підтримки («Яка тут версія?») без обробки паролів.

Чисто мігруйте: „Read old, write new“ statt Big Bang

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

Мультипотоки та стрімінг: типові проблемні місця в Delphi

Шифрування часто виконується у воркер-потоках (наприклад, при експорті, при завантаженні в портал клієнта, при записі великих архівів). Два моменти, які регулярно помічаються в Delphi-проектах:

  • Позиції потоку: перед шифруванням/дешифруванням визначте чіткі контракти: вхідний потік читається з поточної позиції, вихідний потік записується з поточної позиції. При повторному використанні потоків обовязково явно встановлюйте Position := 0.
  • Піки памяті: уникайте «все в TBytes». Підхід на основі потоків особливо важливий для великих файлів. Якщо ваша крипто-бібліотека приймає лише масиви байтів, варто вкластися в потокову реалізацію або побудувати буферизований адаптер.

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

Коли цей підхід має сенс — і де він може зазнати невдачі

Виправдано, якщо ви: (a) довгостроково зберігаєте зашифровані дані експорту/імпорту, (b) одночасно експлуатуєте різні версії програми, (c) обробляєте дані як потоки або (d) потребуєте чистого крипто-інтерфейсу для кількох модулів (Client/Server/Tooling).

Провалюється, якщо ви намагаєтесь вирішити цим «усе»: за транспорт відповідає TLS, а не саморобний AES-обгорток. Для секретів (паролі, токени) часто доцільні OS-Secret-Store або Vault. І якщо потрібна сумісність з іншими мовами, потрібно точно документувати заголовок, порядок байтів (Endianness) і кодування (або використовувати встановлений формат).

Висновок: AES в Delphi — це не стільки алгоритм, скільки інженерія

Справжня цінність цього фрагмента — не «AES працює», а експлуатаційно придатний формат: випадковий Salt і IV, версіонований заголовок, параметри PBKDF2 у payload та обробка з підтримкою потоків. Для нових форматів додайте за можливості цілісність (AES-GCM або Encrypt-then-HMAC). Так «ми зашифрували щось» перетворюється на компонент, який у цифрових корпоративних рішеннях залишатиметься придатним для обслуговування та міграції навіть через роки.

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

У фаховому контексті також важливу роль відіграють Delphi Aes і Pbkdf2 Delphi, коли інтеграції, потоки даних і подальший розвиток мають коректно взаємодіяти.

Обговорити проєкт або план модернізації з Net-Base.

Наступний крок

Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.

Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
  • Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.

Поділитися дописом

Поділитися цим дописом безпосередньо

LinkedIn, X, XING, Facebook, WhatsApp та електронна пошта доступні негайно. Для Instagram ми готуємо посилання та короткий текст безпосередньо.

Електронна пошта

Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.