Од теме часописа до пројектне праксе
Одговарајуће странице услуга и техничке странице за чланак
При AES шифровању Delphi у пракси се ретко ради о „самом AES-у“, већ о рубним условима: подаци морају да се обрађују као stream (фајлови, 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) добијате битфлип/паддинг проблеме и тешко дијагностиковате грешке.
Керн овог чланка: мали контејнер-формат који подржава стримовање, верзионисање и избегава уобичајене грешке.
AES шифровање Delphi са заглављем, Salt-ом, IV-ом и PBKDF2
Дефинишемо прост контејнер-формат који се може користити и у BLOB-овима базе података или као payload у порукама:
- Magic: 4 bajta, нпр.
NBAE(брза провера „да ли је ово наш формат?”) - Верзија: 1 bajt (омогућава миграцију)
- KDF-параметри: број итерација (4 bajta)
- Salt: 16 bajta (насумично по фајлу)
- IV: 16 bajta (насумично по фајлу за AES-CBC)
- Ciphertext: шифровани корисни подаци (погодни за стримовање)
Важно: Salt и IV нису тајни. Морају бити само нови за сваку енкрипцију. Лозинка остаје тајна; из ње изведени кључ се не чува.
AES шифровање Delphi у стриму: писање/читање контејнера
Код је намерно написан као „Bauplan“: јасно раздвојене функције, проверљива заглавља, без скривених глобалних променљивих. За 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;
// — Zavisnosti koje treba implementirati zavisno od Crypto-Stack-a —
procedure FillRandomBytes(var B: TBytes);
begin
// Za kripto-slučajnost: koristiti OS-CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Ovde namerno kao rezervna oznaka.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG nije povezan‘);
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementacija npr. pomoću DEC (PBKDF2) ili druge biblioteke.
// Rezultat: AKeyLen bajtova.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: nije povezan‘);
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementacija putem biblioteke:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Važno: obrađivati stream-orijentisano, ne sve u memoriji.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: nije povezan‘);
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: nije povezan‘);
end;
// — Pomoćne rutine —
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Zaglavlje nije moglo biti zapisano‘);
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Zaglavlje nepotpuno‘);
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(‚Nije važeći kontejner (Magic se ne poklapa)‘);
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Nepoznata verzija kontejnera: %d‘, [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Broj iteracija van razumnog opsega‘);
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(‚Lozinka ne sme biti prazna‘);
// Generisanje Salt/IV
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// Popunjavanje zaglavlja
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);
// Izvod ključa (32 bajta za AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// Šifrovanje korisnih podataka (Ciphertext sledi odmah nakon zaglavlja)
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(‚Lozinka ne sme biti prazna‘);
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);
// Dešifrovanje počevši od trenutne pozicije streama (posle zaglavlja)
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.
Svrha: Minimalni kontejner pogodan za fajlove i BLOB-ove, uključujući verzionisanje i parametre KDF. Ograničenja: Morate imati pravu CSPRNG-vezu (kriptografski siguran izvor slučajnih brojeva iz operativnog sistema) i robusnu implementaciju AES/PBKDF2 ispod. Zamke: Ne koristite „bilo koji“ Random (ne Random()), izbegavajte fiksne IV-e i pri dešifrovanju planirajte jasno rukovanje greškama (pogrešna lozinka naspram oštećenih podataka). Varijante: umesto CBC radije AEAD (vidi dole), ili proširite header za ID algoritma i HMAC.
Integritet: zašto je AES-CBC sam po sebi rizičan u produkciji
AES-CBC je i dalje prisutan u mnogim legacy kontekstima i može funkcionisati ako dodatno uvedete proveru integriteta. Bez integriteta napadač može manipulisati šifratom; čak i bez aktivnog napada prenosi greške ili oštećeni slojevi skladištenja mogu izazvati teško dijagnostikovane „padding“ greške.
Pragmatične opcije:
- Encrypt-then-HMAC: Nakon šifrata upišite HMAC (npr. HMAC-SHA-256) preko hedera+šifrata. Pri čitanju prvo proverite HMAC, pa onda dešifrujte. Za to je idealno izvesti dva ključa iz PBKDF2 (npr. 64 bajta: 32 za AES, 32 za HMAC), umesto da isti ključ koristite dvaput.
- AES-GCM: AEAD režim (Authenticated Encryption with Associated Data). Isporučuje šifrat + auth-tag. To je danas često najsimpatičniji izbor, ako vaša Delphi-biblioteka stabilno podržava GCM. Polja hedera mogu se autentifikovati kao „AAD“ bez potrebe da ih šifrujete.
Ako morate ostati na CBC (npr. zbog interoperabilnosti), Encrypt-then-HMAC je robustno dopunsko rešenje. Za nove formate isplati se GCM, jer time dobijate autentifikaciju i slike grešaka postaju jasnije.
Neobično važno: „Kryptografischer Zufall“ und warum System.Hash nicht reicht
Čest legacy refleks u Delphi-projektima: „Samo uradimo SHA256 preko vremenskog žiga + nečega i imamo Random.“ To nije pouzdana osnova. Za salt i IV trebate CSPRNG (Cryptographically Secure Pseudo Random Number Generator) operativnog sistema. Na Windows je to tipično BCrypt-API (CNG), na Linux generator u kernelu kao što su getrandom() ili /dev/urandom. Praktična razlika je sledeća: CSPRNG je dizajniran tako da iz posmatranih vrednosti nije moguće predvideti naredne vrednosti.
Arhitektonski trik: Kapsulirajte to u malu „RandomProvider“-unit koju možete mock-ovati u testovima. Time rešavate dva slučaja: reproduktivne testove (sa fiksnim seed-om u mocku) i stvarnu bezbednost u produkciji (sa OS-CSPRNG). Tako sprečavate da u hitnom popravku „samo tako“ ponovo uđe Random() zato što je brzo.
Debugging und Legacy-Migration: Versionierung ist kein Luxus
Header nije samo zbog „kriptografske lepote“, već zbog održivosti:
- Iteration Tuning: Broj PBKDF2 iteracija menja se tokom godina. Sa poljem u headeru možete kasnije povećati vrednosti, bez da učinite stare podatke nečitljivim.
- Formatwechsel: Verzija 2 može, na primer, preći na AES-GCM ili dodati HMAC.
- Diagnose im Feld: Magic/Version omogućavaju brze provere u logovima i alatima, bez potrebe za dešifrovanjem podataka.
Практичан савет: Имплементирајте мали „инспектор“ који чита само хедер (Magic/Version/Iterations) и уписује га у лог. То решава многе случајеве подршке („Која верзија је овде?“) без руковања лозинкама.
Чиста миграција: „Read old, write new“ уместо Big Bang
Ако уклањате стари формат (нпр. фиксни IV, без KDF-а, Blowfish/3DES или самоградњени XOR), у Delphi-пројектима се показао следећи образац: при читању препознајете више формата (Magic/Version или fallback-хеуристика), при писању генеришете само нови формат. Додатно, при успешној декрипцији можете у позадини поново шифровати („lazy migration“), ако то одговара процесу. Тако смањујете ризик при rollout-у и избегавате „поново све шифровати“ као прозор одржавања.
Нитовање и стриминг: типичне граничне тачке у Delphi
Шифровање често ради у радним нитима (нпр. при експорту, при отпремању у клијентски портал, при писању великих архива). Два аспекта која се у Delphi-пројектима редовно уочавају:
- Позиције стрима: Пре шифровања/дешифровања јасни уговори: улазни стрим се чита од тренутне позиције, излазни стрим се пише од тренутне позиције. При поновној употреби стримова обавезно намерно подесите
Position := 0. - Пикови меморије: Избегавајте „све у TBytes“. Приступ заснован на стримовима је пресудан за велике датотеке. Ако ваша крипто-библиотека прихвата само бајт-низове, вреди уложити додатни рад да пређете на стрим-способну имплементацију или да изградите буферирани адаптер.
Ако шифрујете у сервисима (Windows- или Linux-Services), обратите пажњу и на прецизно логовање изузетака: „неисправна лозинка“, „хедер оштећен“, „Tag/HMAC неважећи“ су различити оперативни случајеви и треба да буду разликовани. Важно: поруке о грешкама споља не смеју бити прекомерно детаљне (није прикладно јављати „Padding falsch in Block 7“ као API-гршку), али унутрашњи лог може садржати потребне детаље.
Када се приступ исплати — и где може да закаже
Исплати се, ако ви: (a) дугорочно складиштите шифроване податке за извоз/увоз, (b) покрећете паралелно различите верзије програма, (c) обрађујете податке као стримове или (d) требате чисту крипто-интерфејс за више модула (Client/Server/Tooling).
Неће успети, ако покушавате тиме да решите „све“: за транспорт је надлежан TLS, не самоградњени AES-Wrapper. За секрете (лозинке, токени) често је прикладнији OS-Secret-Store или Vault. И ако вам је потребна интероперабилност са другим језицима, морате тачно документовати хедер, Endianness и кодирање (или користити утврђени формат).
Закључак: AES у Delphi је мање алгоритам, више инжењеринг
Прави добитак овог исечка није „AES ради“, већ оперативни формат: случајни salt и IV, верзионисани хедер, PBKDF2-параметри у payload-у и обрада прилагођена стримовима. За нове формате, где год је могуће, допуните интегритет (AES-GCM или Encrypt-then-HMAC). Тако „ми нешто шифрујемо“ постаје грађевни блок који у дигиталним корпоративним решењима и после година остаје одржив и могућ за миграцију.
Ако треба да интегришете такав контејнер у постојећу Delphi-ландшафт или га чисто мигрирате са наслеђеног формата, исплати се кратка провера архитектуре (управљање кључевима, верзије формата, рад/логовање). Детаље радо разјашњавамо у разговору по потреби:
У стручном контексту, Delphi Aes и Pbkdf2 Delphi такође имају важну улогу када интеграције, токови података и даљи развој треба да беспрекорно функционишу.
Разговарајте о пројекту или плану модернизације са Net-Base.
Следећи корак
Када тема прерасте у реалан пројекат, архитектуру, постојеће системе и операције треба рано разматрати заједно.
Подржавамо не само у појединачним питањима, већ и када из исечака изворног кода, застарелих тема или идеја за портале треба да настане поуздан корпоративни пројекат.
- Постојеће стање, циљано стање и технички ризици оцењују се заједно.
- REST, приступ подацима, портали и роллаут се неће одлагати као накнадне последице.
- Ви рано видите који пут је економски и оперативно одржив.