Od teme v reviji do projektne prakse
Ustrezne strani storitev in tehnični opisi k prispevku
Pri AES šifriranju Delphi v praksi redko spodleti „AES samo po sebi“, temveč robni pogoji: podatke je treba obdelovati kot tok (datoteke, BLOBs, varnostne kopije), stari formati morajo ostati berljivi, in v obratovanju potrebujete možnost razhroščevanja (zaglavje, različiciranje) ter varne privzete nastavitve (Salt/IV naključni, brez ponovne uporabe). Ta izvleček iz izvorne kode zato ne prikazuje le „Encrypt/Decrypt“, temveč majhen, zanesljiv format z zaglavjem, različico, Salt in IV – plus PBKDF2 za izpeljavo ključa in mesto, kjer je smiselno dopolniti integriteto.
Zakaj „AES-String verschlüsseln“ skoraj nikoli ni dovolj
V prilagojeni podjetniški programski opremi se šifriranje običajno pojavi na treh mestih: (1) konfiguracija/skrivnosti (npr. dostopni podatki), (2) izmenjevalne/izvozne datoteke in (3) počivalni podatki (npr. arhivi, vsebniki dokumentov). Naiven pristop „geslo → AES-ključ → niz notri/ven“ hitro zataji:
- Ponovna uporaba IV: Pri načinih, kot sta CBC ali GCM, mora biti inicializacijski vektor (IV) za vsako šifriranje edinstven. Konstanten IV pomeni puščanje, tudi če je geslo močno.
- Ključ iz gesla brez KDF: Uporaba gesla neposredno kot ključa (ali enkratno hashanje) omogoča offline napade. KDF (Key Derivation Function), kot je PBKDF2, namerno upočasni napadalce.
- Ni različice formata: Brez zaglavja/različice komaj lahko pozneje spremenite število iteracij, algoritem ali parametre, ne da bi stare podatke „osiroteli“.
- Ni integritete: AES-CBC šifrira, vendar ne preprečuje manipulacij. Brez avtentikacije (npr. HMAC ali AEAD, kot je GCM) boste naleteli na težave z bitflippingom/paddingom in težko diagnostikljivimi napakami.
Kern tega prispevka: majhen kontejnerski format, ki podpira pretakanje, omogoča verzioniranje in se izogne pogostim napakam.
AES šifriranje Delphi z zaglavjem, Salt, IV in PBKDF2
Opredelimo preprost kontejnerski format, ki ga je mogoče uporabiti tudi v BLOB-ih baze podatkov ali v vsebinah sporočil:
- Magic: 4 bajti, npr.
NBAE(hitro preverjanje ‚Je to naš format?‘) - Različica: 1 bajt (omogoča migracijo)
- KDF-Parameter: število iteracij (4 bajti)
- Salt: 16 bajtov (naključno za vsako datoteko)
- IV: 16 bajtov (naključno za vsako datoteko za AES-CBC)
- Šifrotekst: šifrirani uporabniški podatki (podpora pretakanju)
Pomembno: Salt in IV nista skrivnostna. Morata biti le za vsako šifriranje nova. Geslo ostane skrivno; iz njega izpeljan ključ ni shranjen.
AES šifriranje Delphi v toku: pisanje/branje kontejnerja
Koda je namerno zapisana kot ’načrt‘: jasno ločene funkcije, preverljiva zaglavja, brez skritih globalnih spremenljivk. Za AES in PBKDF2 številne ekipe uporabljajo uveljavljeno kriptografsko knjižnico (npr. DEC). Izvleček pokaže format in vzorec pretakanja; klice AES/PBKDF2 je mogoče tako zapakirati, da jih lahko zamenjate glede na uporabljeno knjižnico.
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
// Für Kryptozufall: OS-CSPRNG verwenden (Windows BCryptGenRandom,
// Linux getrandom/urandom). Hier absichtlich als Platzhalter.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG ni povezan‘);
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementierung z. B. mit DEC (PBKDF2) oder anderer Bibliothek.
// Ergebnis: AKeyLen Bytes.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: ni implementiran‘);
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementierung per Bibliothek:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Wichtig: Stream-orientiert verarbeiten, nicht alles in Memory.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: ni implementiran‘);
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: ni implementiran‘);
end;
// — Helper —
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Zaglavje ni bilo mogoče zapisati‘);
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Zaglavje je nepopolno‘);
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(‚Ni veljaven kontejner (Magic se ne ujema)‘);
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Neznana različica kontejnerja: %d‘, [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Število iteracij izven razumn\u00edh meja‘);
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(‚Geslo ne sme biti prazno‘);
// 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(‚Geslo ne sme biti prazno‘);
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.
Namen: Minimalen vsebnik, primeren za datoteke in BLOB‑e, vključno z verzioniranjem in parametri KDF. Omejitve: Zagotoviti morate pravo CSPRNG‑povezavo (kriptografsko varen naključni vir iz operacijskega sistema) in robustno AES/PBKDF2‑implementacijo pod tem. Past: Ne uporabljajte „katerokoli“ Random (ne Random()), brez fiksnih IV‑jev, in pri dešifriranju načrtujte jasno obravnavo napak (napačno geslo proti poškodovanim podatkom). Variante: namesto CBC raje AEAD (glej spodaj), ali razširite glavo z ID algoritma in HMAC.
Integriteta: zakaj je AES‑CBC sam po sebi v obratovanju preveč tvegan
AES‑CBC je v številnih legacy kontekstih še prisoten in lahko deluje, če uporabite dodatno preverjanje integritete. Brez integritete lahko napadalec manipulira s šifratekstom; tudi brez aktivnega napadalca povzročijo napake pri prenosu ali okvarjene plasti za shranjevanje težko diagnosticirane „Padding“‑napake.
Pragmatične možnosti:
- Encrypt‑then‑HMAC: Po šifratekstu zapišite HMAC (npr. HMAC‑SHA‑256) čez glavo+šifratekst. Pri branju najprej preverite HMAC, nato dešifrirajte. Za to je idealno iz PBKDF2 izpeljati dva ključa (npr. 64 bajtov: 32 za AES, 32 za HMAC), namesto da isti ključ uporabite dvojno.
- AES‑GCM: AEAD način (Authenticated Encryption with Associated Data). Zagotavlja šifratekst + avtentikacijski tag. To je danes pogosto najčistejša izbira, če vaša Delphi‑knjižnica stabilno podpira GCM. Polja v glavi je mogoče kot „AAD“ vključiti v avtentikacijo, brez da bi jih morali šifrirati.
Če morate ostati pri CBC (npr. zaradi interoperabilnosti), je Encrypt‑then‑HMAC robustna dopolnitev. Za nove formate se GCM izplača, ker omogoča vgrajeno avtentikacijo in naredi napake bolj jasno prepoznavne.
Nenavadno pomembno: „Kriptografsko naključje“ in zakaj System.Hash ni dovolj
Pogost legacy refleks v Delphi‑projektih: „Sprejmemo preprosto SHA256 nad časovnim žigom + nekaj in imamo Random.“ To ni zanesljiva osnova. Za salt in IV potrebujete CSPRNG (Cryptographically Secure Pseudo Random Number Generator) operacijskega sistema. Pod Windows je to običajno BCrypt‑API (CNG), pod Linux pa generator v jedru, kot je getrandom() oziroma /dev/urandom. Razlika je praktična: CSPRNG je zasnovan tako, da iz opaženih vrednosti ni mogoče napovedati nadaljnjih vrednosti.
Arhitekturni trik: zapakirajte to v majhno enoto „RandomProvider“, ki jo lahko v testih mockate. Tako rešite dva robna primera hkrati: reproducibilne teste (s fiksnim seedom v mocku) in pravo varnost v produkcijskem okolju (z OS‑CSPRNG). Tako preprečite, da bi v hitrem hotfixu spet „kar na hitro“ uvedli Random(), ker je hitreje.
Razhroščevanje in migracija zapuščene kode: verzioniranje ni luksuz
Glava ni namenjena le „kripto‑lepoti“, ampak vzdrževanju:
- Nastavitev iteracij: Število iteracij PBKDF2 se skozi leta spreminja. S poljem v glavi lahko kasneje povečate iteracije, ne da bi stare podatke naredili neberljive.
- Sprememba formata: Različica 2 bi se lahko npr. preklopila na AES‑GCM ali dodala HMAC.
- Diagnoza v polju: Magic/Version omogočata hitre preveritve v dnevnikih in orodjih, brez dešifriranja podatkov.
Praktični nasvet: Implementirajte majhen „inspektor“, ki prebere le header (Magic/Version/Iterations) in ga zapiše v log. S tem razjasnite številne podporne primere („Katera različica je tukaj?“) brez upravljanja gesel.
Čista migracija: „Read old, write new“ namesto Big Bang
Če zamenjujete star format (npr. fiksni IV, brez KDF, Blowfish/3DES ali lastni XOR), se je v Delphi-projektih izkazal naslednji vzorec: pri branju prepoznate več formatov (Magic/Version ali fall-back heuristika), pri pisanju pa ustvarjate le nov format. Dodatno lahko ob uspešnem dešifriranju v ozadju ponovno šifrirate („lazy migration“), če to ustreza procesu. Tako zmanjšate tveganje pri rolloutu in se izognete oknu vzdrževanja »enkrat vse znova šifrirati«.
Večnitenje in pretakanje: tipične kritične točke v Delphi
Šifriranje pogosto teče v delovnih nitih (npr. pri izvozu, pri nalaganju v portal za stranke, pri pisanju velikih arhivov). Dve točki, ki se v Delphi-projektih redno pojavljata:
- Pozicije tokov: Pred šifriranjem/dešifriranjem določite jasne pogodbe: vhodni stream se bere od trenutne pozicije, izhodni stream se piše od trenutne pozicije. Pri ponovni uporabi streamov obvezno zavestno nastavite
Position := 0. - Vrhi porabe pomnilnika: Izogibajte se rešitvam „vse v TBytes“. Pristop s streami je ključen pri velikih datotekah. Če vaša kriptografska knjižnica sprejema le byte-polja, se izplača dodatno delo in prehod na stream-kompatibilno implementacijo ali izdelava predpomnilniškega adapterja.
Če šifrirate v storitvah (Windows- ali Linux-Services), pazite tudi na urejeno beleženje izjem: „napačno geslo“, „header pokvarjen“, „Tag/HMAC neveljaven“ so različni obratovalni primeri in jih je treba razlikovati. Pomembno: sporočila o napakah ne smejo navzven razkrivati preveč podrobnosti (ne „Padding napačen v bloku 7“ kot API-napaka), v internem logu pa naj bodo podrobnejša.
Kdaj se pristop izplača – in kje lahko odpove
Izplača se, če: (a) trajno shranjujete šifrirane izvozno-/uvozne podatke, (b) vzporedno poganjate različne različice programa, (c) obdelujete podatke kot streame ali (d) potrebujete čisto kripto-vmesnik za več modulov (client/server/tooling).
Odpove, če poskušate z njim rešiti „vse“: za transport je odgovoren TLS, ne lastnoročno izdelan AES-ov ovojnica. Za secrets (gesla, tokeni) je pogosto bolj smiseln OS-Secret-Store ali Vault. In če potrebujete interoperabilnost z drugimi jeziki, morate header, endianness in encoding natančno dokumentirati (ali uporabiti uveljavljen format).
Sklep: AES v Delphi je manj algoritem, več inženiring
Resnična vrednost tega kosa ni le „AES deluje“, ampak operativno uporaben format: naključen salt in IV, verzioniran header, PBKDF2-parametri v payloadu in obdelava prijazna streamom. Za nove formate dodajte čim več integritete (AES-GCM ali Encrypt-then-HMAC). Tako se iz „samo nekaj šifriramo“ ustvari gradnik, ki je v digitalnih podjetniških rešitvah tudi po letih še vzdržen in migrirljiv.
Če morate takšen kontejner integrirati v uveljavljeno Delphi okolje ali ga čisto migrirati iz zastarelega formata, se izplača kratek arhitekturni pregled (upravljanje ključev, različice formata, obratovanje/logiranje). Podrobnosti po potrebi z veseljem razjasnimo v pogovoru:
V strokovnem okolju imajo tudi Delphi Aes in Pbkdf2 Delphi pomembno vlogo, kadar morajo integracije, podatkovni tokovi in nadaljnji razvoj tesno usklajeno delovati.
O projektu ali modernizacijskem načrtu se pogovorite z Net-Base.
Naslednji korak
Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.
Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.
- Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
- REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
- Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.