Net-Base списание

30.05.2026

AES-шифрирање во Delphi: робустен пример на изворен код со IV, Salt, Header и Streaming

Практичен пример на изворен код Delphi за AES-шифрирање со случаен salt и IV, јасна структура на заглавието на датотеката, изведување на клуч преку PBKDF2 и стриминг — вклучувајќи типични замки кај legacy-формати, прашања за интегритет и оперативно работење.

30.05.2026

Од тема во магазинот до проектна пракса

Соодветни страници за услуги и технички информации поврзани со објавата

Кај AES шифрирање Delphi во практика ретко се „куца“ проблемот во самиот AES, туку во работните услови: податоците мора да се обработуваат како поток (фајлови, BLOB-ови, бекапи), старите формати треба да останат читливи, а во оперативна употреба ви треба дебагабилност (header, верзионирање) и сигурни подразбирани вредности (Salt/IV случајни, без повторна употреба). Затоа овој исечок од изворен код не покажува само „Encrypt/Decrypt“, туку мало, робустно форматирање со header, верзија, Salt и IV – плус PBKDF2 за изведување на клучот и место каде што е соодветно да се додаде интегритет.

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

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

  • Повторна употреба на IV: Во режими како CBC или GCM, иницијализирачки вектор (IV) мора да биде уникатен за секоја шифрирање. Константен IV е протекување, дури и ако лозинката е силна.
  • Клуч од лозинка без KDF: Директно користење на лозинка како клуч (или еднаш хеширан) ги поканува офлајн нападите. KDF (Key Derivation Function) како PBKDF2 целно ги успорува напаѓачите.
  • Без верзија на формат: Без header/верзија тешко ќе можете подоцна да го смените бројот на итерации, алгоритамот или параметрите без да ги оставите старите податоци „осиромашени“.
  • Без интегритет: AES-CBC шифрира, но не спречува манипулација. Без аутентификација (на пр. HMAC или AEAD како GCM) добивате битфлип/паддинг проблеми и тешко диагностикливи грешки.

Клучот на овој напис: мало контејнер-форматче кое поддржува стримирање, е верзионирано и ги избегнува типичните грешки.

AES шифрирање Delphi со header, Salt, IV и PBKDF2

Дефинираме едноставен контејнер-формат што може да се користи и во BLOB-ови на база податоци или во message-payloads:

  • Magic: 4 бајта, н. пр. NBAE (брз „Дали е ова нашиот формат?“-проверка)
  • Version: 1 бајт (можност за миграција)
  • KDF-параметри: број на итерации (4 бајта)
  • Salt: 16 бајта (случајно за секој фајл)
  • IV: 16 бајта (случајно за секој фајл за AES-CBC)
  • Ciphertext: шифрирани кориснички податоци (погодни за стримирање)

Важно: Salt и IV не се тајни. Тие само мора да бидат нови за секое шифрирање. Лозинката останува тајна; од неа изведениот клуч не се зачувува.

AES шифрирање Delphi во поток: запишување/читање на контејнер

Кодот е свесно напишан како „план за градење“: јасно одвоени функции, проверливи header-и, нема скриени глобали. За 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;

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

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 пополнување (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;

// — Helper —

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 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()), нема фиксни IVs, и при дешифрирање предвидете јасно ракување со грешки (погрешна лозинка vs. оштетени податоци). Варијанти: наместо CBC подобро AEAD (види подолу), или проширете го заглавјето со Algorithmus-ID и HMAC.

Интегритет: зошто AES-CBC сам по себе во експлоатација е премногу ризичен

AES-CBC сѐ уште е присутен во многу наследени контексти и може да функционира ако дополнително користите механизам за обезбедување на интегритет. Без интегритет напаѓач може да го манипулира шифротекстот; дури и без активен напаѓач, грешки при пренос или дефектни слоеви за складирање создаваат тешко дијагностикливи „padding“-грешки.

Прагматични опции:

  • Encrypt-then-HMAC: По шифротекстот запишете HMAC (на пр. HMAC-SHA-256) над заглавјето+шифротекстот. При читање прво проверете HMAC, потоа дешифрирајте. За тоа идеално изведете две клучеви од PBKDF2 (на пр. 64 бајти: 32 за AES, 32 за HMAC), наместо да го користите истиот клуч двојно.
  • AES-GCM: AEAD-мод (Authenticated Encryption with Associated Data). Дава шифротекст + Auth-Tag. Ова денес често е најпригодна опција, ако вашата Delphi-библиотека стабилно ја поддржува GCM. Полето(ата) од заглавјето можат да се аутентификуваат како „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 генератор во kernel-от како getrandom() односно /dev/urandom. Практичната разлика е: CSPRNG е дизајниран така што од набљудувани вредности не може да се предвиди следниот низ вредности.

Архитектонски трик: Капсулирајте го тоа во мала „RandomProvider“-Unit која можете да ја мокнете во тестовите. Така решавате две работи одеднаш: репродуцирачки тестови (со фиксна семка во mock-от) и вистинска безбедност во продукциската експлоатација (со OS-CSPRNG). На тој начин го спречувате сценариото во кое во некој hotfix „само така“ повторно се внесе 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 или fallback-евристика), при пишување произведувате само новиот формат. Дополнително, може да извршите ре-шифрирање на заднина по успешно дешифрирање („lazy migration“), ако тоа одговара на процесот. Така го намалувате ризикот при rollout и избегнувате „сè еднаш да се шифрира повторно“ како одржувачко прозорче.

Threading und Streaming: typische Kanten in Delphi

Шифрирањето често се изведува во worker-нишки (на пр. при експорт, при аплоад во Kundenportal, при пишување големи архиви). Два аспекта кои во Delphi-проектите редовно испливаат:

  • Stream-Positionen: Пред шифрирање/дешифрирање дефинирајте јасни договори: input-stream се чита од тековната позиција, output-stream се запишува од тековната позиција. Ако повторно користите stream-ови, свесно иницијализирајте Position := 0.
  • Memory-Spitzen: Избегнувајте „сè во TBytes“. Пристапот базиран на stream е клучен за големи фајлови. Ако вашата крипто-библиотека прифаќа само byte-array, е вредна дополнителната работа да се премине на stream-способна имплементација или да се изгради наточен адаптер со буфер.

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

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

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

Пропаѓа ако се обидувате со тоа да решите „сè“: за transport одговорен е TLS, не некој сопствен AES-wrapper. За secrets (лозинки, token-и) често е поразумно да се користи OS-Secret-Store или Vault. И ако ви треба интероперабилност со други јазици, мора да ги документирате хедерите, endianness и encoding прецизно (или да користите веќе утврден формат).

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

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

Ако треба да интегрирате таков контејнер во постоечка Delphi-ландшафт или да мигрирате чисто од наследен формат, се исплаќа кратка архитектонска проверка (управување со клучеви, верзии на формати, експлоатација/логирање). Детали ќе разјасниме по потреба во разговор:

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

Разговарајте за проект или план за модернизација со Net-Base.

Следен чекор

Кога темата ќе прерасне во реален проект, архитектурата, постоечката средина и експлоатацијата треба рано да се разгледаат заедно.

Не поддржуваме само при поединечни прашања, туку и кога од исечоци од изворен код, legacy-теми или идеи за портали треба да прерасне во робустен корпоративен проект.

  • Постоечката состојба, целната слика и техничките ризици се проценуваат заедно.
  • REST, пристапот до податоци, порталите и Rollout не се одложуваат како подоцнежни последици.
  • Уште рано идентификувате кој пат е економски и оперативно одржлив.

Сподели објава

Споделете го овој пост директно.

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта се веднаш достапни. За Instagram директно подготвуваме линк и краток текст.

Е-пошта

Instagram се отвора во нов таб. Линкот и краткиот текст претходно се копираат во меѓуспремникот.