Od teme magazina do projektne prakse
Povezane stranice usluga i tehnologije za članak
U praksi kod AES šifrovanja Delphi rijetko je problem „sam AES“, već rubni uvjeti: podaci moraju biti obrađeni kao stream (datoteke, BLOB-ovi, backupi), stari formati moraju ostati čitljivi, a u radu su potrebni mogućnosti debugovanja (zaglavlje, verzionisanje) i sigurni podrazumevani izbori (Salt/IV nasumično, bez ponovne upotrebe). Ovaj isječak izvornog koda zato ne prikazuje samo „Encrypt/Decrypt“, već mali, robusni format sa zaglavljem, verzijom, Salt i IV – plus PBKDF2 za izvodjenje ključa i mjesto na kojem je smisleno dopuniti integritet.
Zašto „šifriranje AES-stringa“ gotovo nikad nije dovoljno
U prilagođenom poslovnom softveru enkripcija se tipično pojavljuje na tri mjesta: (1) konfiguracija/Secrets (npr. pristupni podaci), (2) razmjena/eksport fajlova i (3) podaci u mirovanju (npr. arhive, kontejneri dokumenata). Naivni pristup „lozinka → AES-Key → String unutra/van“ brzo puca:
- Ponovna upotreba IV: Kod režima poput CBC ili GCM inicijalizacioni vektor (IV) mora biti jedinstven za svaku enkripciju. Konstantan IV predstavlja curenje, čak i ako je lozinka jaka.
- Ključ iz lozinke bez KDF: Korištenje lozinke direktno kao ključa (ili jednokratno hashiranje) omogućava offline napade. KDF (Key Derivation Function) poput PBKDF2 ciljano usporava napadača.
- Nema verzije formata: Bez zaglavlja/verzije teško je kasnije promijeniti broj iteracija, algoritam ili parametre bez da stari podaci postanu neupotrebljivi.
- Nedostatak integriteta: AES-CBC šifrira, ali ne spriječava manipulaciju. Bez autentifikacije (npr. HMAC ili AEAD kao GCM) dobijate bitflipping-/padding-probleme i teško dijagnostikovane greške.
Suština ovog doprinosa: mali kontejnerski format koji podržava streaming, može se verzionisati i izbjegava uobičajene greške.
AES šifrovanje Delphi sa zaglavljem, Salt, IV i PBKDF2
Definišemo jednostavan kontejnerski format koji se može koristiti i u BLOB-ovima baze podataka ili u message-payloadima:
- Magic: 4 Bytes, npr.
NBAE(brza provjera „da li je ovo naš format?“) - Version: 1 Byte (omogućava migraciju)
- KDF-Parameter: broj iteracija (4 Bytes)
- Salt: 16 Bytes (nasumično po datoteci)
- IV: 16 Bytes (nasumično po datoteci za AES-CBC)
- Ciphertext: šifrirani korisni podaci (podržava streaming)
Važno: Salt i IV nisu tajni. Moraju biti novi za svaku enkripciju. Lozinka ostaje tajna; iz nje izvedeni ključ se ne pohranjuje.
AES šifrovanje Delphi u streamu: pisanje/čitanje kontejnera
Kod je namjerno pisan kao „bauplan“: jasno odvojene funkcije, provjerljiva zaglavlja, nema skrivenih globalnih varijabli. Za AES i PBKDF2 mnogi timovi koriste provjerenu kripto-biblioteku (npr. DEC). Isječak prikazuje format i streaming-patern; pozivi AES/PBKDF2 su tako enkapsulirani da ih možete zamijeniti prema izabranoj biblioteci.
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 trebate implementirati u zavisnosti od Crypto-stacka ---
procedure FillRandomBytes(var B: TBytes);
begin
// Za kriptografski slučajni broj: koristiti OS-CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Ovdje namjerno ostavljeno kao zamjena.
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. s DEC (PBKDF2) ili drugom bibliotekom.
// 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: obrada treba biti orijentisana na tok (stream), 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 se nije moglo zapisati');
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 izvan prihvatljivih granica');
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 smije biti prazna');
// Salt/IV generisati
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// Popuniti zaglavlje
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);
// Izvesti ključ (32 bajta za AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// Šifrirati korisne podatke (ciphertext slijedi 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 smije 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);
// Dekriptovati od trenutne pozicije u streamu (nakon 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 KDF-parametre. Ograničenja: Morate koristiti pravu CSPRNG-vezu (kriptografski siguran izvor slučajnosti iz operativnog sistema) i robusnu AES/PBKDF2-implementaciju ispod. Zamke: Nemojte koristiti „bilo koji“ Random (ne Random()), bez fiksnih IV-ova, i pri dekripciji predvidite jasno rukovanje greškama (pogrešna lozinka naspram oštećenih podataka). Varijante: umjesto CBC radije AEAD (vidi dolje), ili proširiti header za ID algoritma i HMAC.
Integritet: warum AES-CBC allein im Betrieb zu riskant ist
AES-CBC je i dalje prisutan u mnogim legacy kontekstima i može funkcionisati ako dodatno koristite provjeru integriteta. Bez integriteta napadač može manipulisati cipherteksom; čak i bez aktivnog napada, greške u prijenosu ili oštećeni slojevi skladištenja stvaraju teško dijagnosticirajuće „padding“-greške.
Pragmatske opcije:
- Encrypt-then-HMAC: Nakon ciphertexta zapišite HMAC (npr. HMAC-SHA-256) preko Header+Ciphertext. Pri čitanju prvo provjerite HMAC, zatim dešifrirajte. Za to izvedite idealno dva ključa iz PBKDF2 (npr. 64 bajta: 32 za AES, 32 za HMAC), umjesto korištenja istog ključa dvaput.
- AES-GCM: AEAD-mod (Authenticated Encryption with Associated Data). Daje Ciphertext + Auth-Tag. To je danas često najčistiji izbor, ako vaša Delphi-biblioteka stabilno podržava GCM. Polja headera se mogu autentificirati kao „AAD“ bez da ih šifrirate.
Ako morate ostati pri CBC (npr. zbog interoperabilnosti), Encrypt-then-HMAC je robusno dopunjavanje. Za nove formate isplati se GCM, jer dobijate autentifikaciju „uz put“ i jasnije obrasce grešaka.
Ungewöhnlich wichtig: „Kryptografischer Zufall“ und warum System.Hash nicht reicht
Čest legacy-refleks u Delphi-projektima: „Uzmemo jednostavno SHA256 nad timestampom + nečim i imamo Random.“ To nije pouzdana osnova. Za salt i IV trebate CSPRNG (Cryptographically Secure Pseudo Random Number Generator) operativnog sistema. Pod Windows to je tipično BCrypt-API (CNG), pod Linux generator u kernelu poput getrandom() odnosno /dev/urandom. Razlika je praktična: CSPRNG je dizajniran tako da se iz promatranih vrijednosti ne mogu predvidjeti naredne vrijednosti.
Arhitektonski trik: Kapsulirajte to u malu „RandomProvider“-unitu koju možete mockati u testovima. Time rješavate odmah dva slučaja: reproduktivne testove (sa fiksnim seedom u mocku) i pravu sigurnost u produkciji (sa OS-CSPRNG). Tako spriječite da u hotfixu „samo tako“ opet završi Random(), jer je brzo i jednostavno.
Debugging und Legacy-Migration: Versionierung ist kein Luxus
Header nije samo za „krypto-finu“ strukturu, već i za održavanje:
- Iteration Tuning: PBKDF2-broj iteracija se mijenja tokom godina. Sa poljem u headeru kasnije možete povećati broj iteracija bez da stare podatke učinite nečitljivim.
- Formatwechsel: Verzija 2 bi, npr., mogla preći na AES-GCM ili dodati HMAC.
- Diagnose im Feld: Magic/Version omogućavaju brze provjere u logovima i alatima bez dešifriranja podataka.
Praktičan savjet: Implementirajte mali „Inspector“, koji čita samo header (Magic/Version/Iterations) i zapisuje u log. Tako razjasnite mnoge slučajeve podrške („Koja je ovdje verzija?“) bez rukovanja lozinkama.
Sauber migrieren: „Read old, write new“ statt Big Bang
Ako želite zamijeniti stari format (npr. fiksni IV, bez KDF, Blowfish/3DES ili vlastito XOR rješenje), u Delphi-projektima se pokazao ovaj obrazac: pri čitanju prepoznajte više formata (Magic/Version ili fallback heuristika), pri pisanju generirajte samo novi format. Dodatno, pri uspješnom dešifriranju možete u pozadini re-encrypten („lazy migration“), ako to odgovara procesu. Time smanjujete rizik pri rollout-u i izbjegavate „sve jednom ponovno enkriptovati“ kao prozor za održavanje.
Threading und Streaming: typische Kanten in Delphi
Enkripcija često radi u worker-threadovima (npr. pri exportu, pri uploadu u portal za klijente, pri pisanju velikih arhiva). Dva aspekta koja se u Delphi-projektima redovno pojavljuju:
- Pozicije streama: Prije enkriptovanja/dekriptovanja definirajte jasne ugovore: input-stream se čita od tekuće pozicije, output-stream se zapisuje od tekuće pozicije. Ako ponovo koristite streamove, obavezno svjesno postavite
Position := 0. - Pikovi memorije: Izbjegavajte „sve u TBytes“. Pristup zasnovan na streamovima posebno je važan za velike fajlove. Ako vaša crypto-biblioteka prihvata samo nizove bajtova, isplati se dodatni napor da se prijeđete na stream-kompatibilnu implementaciju ili izgradite puferski adapter.
Ako enkriptujete unutar servisa (Windows- ili Linux-Services), obratite dodatno pažnju na uredno logovanje izuzetaka: „pogrešna lozinka“, „header oštećen“, „Tag/HMAC nevažeći“ su različiti operativni slučajevi i trebaju biti razdvojivi. Važno: poruke o greškama prema vani ne smiju biti previše detaljne (nema „Padding pogrešan u bloku 7“ kao API-greška), ali interno u logu takve detalje evidentirajte.
Kada se pristup isplati – i gdje može zakazati
Isplati se, ako: (a) trajno pohranjujete enkriptovane export/import podatke, (b) upravljate paralelnim verzijama programa, (c) obrađujete podatke kao streamove ili (d) trebate čist kripto-API za više modula (Client/Server/Tooling).
Može zakazati, ako pokušavate s tim riješiti „sve“: za transport odgovoran je TLS, a ne vlastiti AES-wrapper. Za secrets (lozinke, tokeni) često je prikladniji OS-Secret-Store ili Vault. I ako trebate interoperabilnost s drugim jezicima, morate header, endianness i encoding precizno dokumentirati (ili koristiti etablirani format).
Fazit: AES in Delphi ist weniger Algorithmus, mehr Engineering
Prava vrijednost ovog fragmenta nije „AES radi“, nego operativno upotrebljiv format: nasumični salt i IV, verzionirani header, PBKDF2-parametri u payloadu i stream-kompatibilna obrada. Za nove formate dodajte integritet kad god je moguće (AES-GCM ili Encrypt-then-HMAC). Tako od „mi nešto enkriptujemo“ nastaje komponenta koja u digitalnim poslovnim rješenjima ostaje održiva i migrabilna i nakon godina.
Ako trebate integrisati takav kontejner u postojeću Delphi-okruženje ili ga uredno migrirati iz naslijeđenog formata, isplati se kratak pregled arhitekture (upravljanje ključevima, verzije formata, operativni rad/logiranje). Detalje po potrebi rado razjasnimo u razgovoru:
U stručnom okruženju također Delphi Aes i Pbkdf2 Delphi imaju važnu ulogu, kada integracije, tokovi podataka i dalji razvoj moraju besprijekorno funkcionisati zajedno.
Razgovarajte o projektu ili modernizacijskom poduhvatu sa Net-Base.
Sljedeći korak
Ako se tema pretvori u stvarni projekat, arhitekturu, postojeći sistem i operacije trebalo bi rano zajednički razmotriti.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
- REST, pristup podacima, portali i Rollout neće se odgađati za kasnije faze.
- Pravovremeno prepoznajete koji pristup je ekonomski i operativno održiv.