Від теми журналу до практики проєкту
Відповідні сторінки послуг і технічні сторінки до публікації
У 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, коли інтеграції, потоки даних і подальший розвиток мають коректно взаємодіяти.
Наступний крок
Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.
Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.
- Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
- REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
- Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.