Net-Base Списание

30.05.2026

AES-шифроване в Delphi: надежден фрагмент от изходния код с IV, Salt, Header и стрийминг

Практически използваем Delphi изходен фрагмент за AES-шифроване с произволен salt и IV, ясна структура на файловия хедър, PBKDF2-извеждане на ключ и потоково криптиране – включително типични капани при наследени формати, целостта и експлоатацията.

30.05.2026

От темата в списанието към проектната практика

Подходящи страници за услуги и технологии към публикацията

При AES шифроване Delphi на практика рядко проблемът е „самият AES“, а заобикалящите условия: данните трябва да се обработват като поток (файлове, BLOB-ове, бекъпи), старите формати трябва да останат четими, а в експлоатация са нужни възможности за отстраняване на грешки (заглавен блок, версиониране) и сигурни стойности по подразбиране (Salt/IV случайни, без повторна употреба). Този фрагмент от изходния код показва затова не само „криптиране/декриптиране“, а малък, надежден формат със заглавен блок, версия, Salt и IV – плюс PBKDF2 за извеждане на ключ и място, където целостта може да бъде добавена разумно.

Защо „шифроването на AES-стринг“ почти никога не е достатъчно

В индивидуален корпоративен софтуер шифроването обикновено се появява на три места: (1) конфигурация/секрети (напр. данни за достъп), (2) обменни/експортни файлове и (3) данни в покой (напр. архиви, контейнер за документи). Наивният подход „Парола → AES-ключ → String вътре/вън“ бързо се проваля:

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

Ядрото на този материал: малък контейнерен формат, който поддържа стрийминг, позволява версиониране и избягва стандартните грешки.

AES шифроване Delphi със заглавен блок, Salt, IV и PBKDF2

Дефинираме прост контейнерен формат, който може да се използва и в BLOB-ове в база данни или в payload-и на съобщения:

  • Magic: 4 Bytes, з. б. NBAE (бърза проверка „Това ли е нашият формат?“)
  • Version: 1 Byte (позволява миграция)
  • KDF-Parameter: брой итерации (4 Bytes)
  • Salt: 16 Bytes (случайно за всеки файл)
  • IV: 16 Bytes (случайно за всеки файл за 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;

// --- Зависимости, които трябва да реализирате в зависимост от крипто-стека ---

procedure FillRandomBytes(var B: TBytes);
begin
  // За криптографски случайни стойности: използвайте OS-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 попълване
  // Важно: обработвайте на поток, не всичко в паметта.
  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('Стойността Iterations е извън разумни граници');

  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 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('Паролата не може да бъде празна');

  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, и предвидете ясна обработка на грешки при декриптиране (грешна парола срещу повредени данни). Варианти: вместо CBC предпочитайте AEAD (вж. по-долу), или разширете хедъра с ID на алгоритъма и HMAC.

Интегритет: защо AES-CBC сам по себе си е твърде рисков в експлоатация

AES-CBC все още присъства в много legacy контексти и може да функционира, ако използвате допълнително средство за гаранция на интегритета. Без интегритет атакуващ може да манипулира шифротекста; дори без активен атакуващ, грешки при предаване или дефектни слоеве за съхранение могат да породят трудно диагностицирани „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). Дава ciphertext + auth-tag. Това често е най-чистият избор днес, ако вашата Delphi-библиотека поддържа GCM стабилно. Полета в хедъра могат да бъдат автентикирани като „AAD“, без да ги шифрирате.

Ако трябва да останете на CBC (напр. заради интероп), Encrypt-then-HMAC е здраво допълнение. За нови формати GCM си струва, защото осигурява автентикация „включена“ и прави картината на грешките по-ясна.

Изключително важно: „криптографски случайни числа“ и защо System.Hash не е достатъчен

Често срещан наследствен рефлекс в Delphi-проекти: „Просто вземаме SHA256 върху времеви печат + нещо и имаме Random.“ Това не е надеждна основа. За salt и IV ви трябва CSPRNG (криптографски сигурен псевдослучаен генератор) на операционната система. Под Windows това типично е BCrypt-API (CNG), под Linux — генератор в ядрото като getrandom() или /dev/urandom. Разликата е практическа: CSPRNG е проектиран така, че от наблюдавани стойности да не може да се предвидят следващи стойности.

Архитектурен трик: Капсулйте това в малка „RandomProvider“-Unit, която можете да мокнете в тестове. По този начин решавате два гранични случая наведнъж: възпроизводими тестове (с фиксиран seed в мока) и реална сигурност в продукция (с OS-CSPRNG). Така предотвратявате в един Hotfix „да се вкара“ отново Random(), само защото е бързо.

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

Хедърът не е само за „крипто-естетика“, а за поддръжка:

  • Iteration Tuning: Числото на итерациите на PBKDF2 се променя през годините. С поле в хедъра можете по-късно да го увеличите, без да направите старите данни нечетаеми.
  • Formatwechsel: Версия 2 например може да премине към AES-GCM или да добави HMAC.
  • Diagnose im Feld: Magic/Version позволяват бързи проверки в логове и инструменти, без да е необходимо да дешифрирате данните.

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

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

Ако заменяте стар формат (например фиксиран IV, липса на KDF, Blowfish/3DES или собственоръчно направен XOR), в Delphi-проекти се е доказал следният модел: При четене разпознавате няколко формата (Magic/Version или fallback-хеуристика), при запис вече генерирате само новия формат. Допълнително можете при успешно дешифриране на заден фон да правите пре-шифриране („lazy migration“), ако това пасва на процеса. По този начин намалявате риска при разгръщане и избягвате „всичко да се презшифрира наведнъж“ като прозорец за поддръжка.

Threading und Streaming: typische Kanten in Delphi

Шифроването често се изпълнява в Worker-нишки (например при експорт, при upload в портал на клиента, при запис на големи архиви). Два аспекта, които в Delphi-проекти редовно изпъкват:

  • Stream-Positionen: Преди шифроване/дешифриране ясни контракти: входният стрийм се чете от текущата позиция, изходният стрийм се записва от текущата позиция. При повторна употреба на стримове задължително задайте Position := 0 съзнателно.
  • Memory-Spitzen: Избягвайте „всичко в TBytes“. Подходът със стрийм е критичен за големи файлове. Ако вашата Crypto-Bibliothek приема само Byte-Arrays, струва си допълнителната работа да преминете към стрийм-способна имплементация или да изградите буфериран адаптер.

Ако шифровате в услуги (Windows- или Linux-Services), обърнете също внимание на чисто логване на изключения: „грешна парола“, „хедър повреден“, „Tag/HMAC невалиден“ са различни оперативни случаи и трябва да могат да се различават. Важно: съобщенията за грешка не бива да са твърде детайлни навън (никакво „Padding грешен в блок 7“ като API-грешка), но в логовете вътрешно могат да бъдат по-подробни.

Кога подходът си струва – и къде може да се провали

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

Може да се провали, ако се опитвате да решите „всичко“ с това: за транспорт е отговорен TLS, не собствен AES-обвивка. За секрети (пароли, токени) често е по-подходящ OS-Secret-Store или Vault. И ако ви трябва интероп с други езици, трябва да документирате хедъра, Endianness и Encoding точно (или да използвате утвърден формат).

Заключение: AES в Delphi е по-малко алгоритъм, повече инженеринг

Реалната полза от този фрагмент не е „AES работи“, а оперативно годен формат: случайна сол и IV, версиониран хедър, PBKDF2-параметри в payload-а и стрийм-ориентирана обработка. Добавете за новите формати, където е възможно, интегритет (AES-GCM или Encrypt-then-HMAC). Така от „ние шифрираме нещо“ се получава компонент, който в дигиталните корпоративни решения остава поддържим и мигруем и след години.

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

В експертната област Delphi Aes и Pbkdf2 Delphi също играят важна роля, когато интеграции, потоци от данни и по-нататъшно развитие трябва да работят коректно заедно.

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

Следваща стъпка

Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.

Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

  • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
  • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
  • Виждате рано кой път е икономически и експлоатационно жизнеспособен.

Сподели публикацията

Споделете тази публикация директно

LinkedIn, X, XING, Facebook, WhatsApp и имейл са незабавно достъпни. За Instagram ще подготвим връзка и кратък текст.

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

Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.