Net-Base Lehti

30.05.2026

AES-salaus Delphi: vankka lähdekoodiesimerkki IV:n, suolan, otsikon ja suoratoiston kanssa

Käytännönläheinen Delphi-lähdekoodikatkelma AES-salaukseen satunnaisella suolalla ja IV:llä, selkeällä tiedosto-otsikkorakenteella, PBKDF2-avainderivoinnilla ja streamauksella – sisältäen tyypilliset sudenkuopat legacy-formaateissa, eheyden varmistamisessa ja operoinnissa.

30.05.2026

Lehden aiheesta projektikäytäntöön

Artikkeliin liittyvät palvelu- ja tekniikkasivut

Kun kyse on AES-salauksesta Delphi, käytännön ongelmat eivät yleensä johtu itse AES:stä vaan reunaehdoista: data täytyy käsitellä virtana (tiedostot, BLOBit, varmuuskopiot), vanhojen formaattien on säilyttävä luettavina, ja tuotannossa tarvitaan debuggattavuutta (header, versiointi) sekä turvalliset oletukset (Salt/IV satunnaiset, ei uudelleenkäyttöä). Tämä lähdekoodikatkelma esittelee siksi enemmän kuin pelkän „Encrypt/Decrypt“: pienen, luotettavan kontin formaatin, jossa on header, versio, Salt ja IV – sekä PBKDF2 avaimenjohtoon ja kohta, johon eheys kannattaa lisätä.

Miksi „AES-merkkijonon salaaminen“ tuskin koskaan riittää

Yksilöllisessä yritysohjelmistossa salaus esiintyy tyypillisesti kolmessa paikassa: (1) konfiguraatio/secretit (esim. tunnukset), (2) vaihto-/eksporttitiedostot ja (3) levossa oleva data (esim. arkistot, dokumenttikontit). Naivi lähestymistapa „salasana → AES-avain → merkkijono sisään/ulos“ pettää nopeasti:

  • IV:n uudelleenkäyttö: CBC- tai GCM-tyyppisissä tiloissa alustamisvektorin (IV) on oltava jokaiselle salaukselle yksilöllinen. Kiinteä IV on vuoto, vaikka salasana olisi vahva.
  • Avain salasanasta ilman KDF:ää: Salasanan käyttäminen suoraan avaimena (tai kertaluonteinen hash) kutsuu offline-hyökkäyksiä. Avainperustusfunktio (KDF) kuten PBKDF2 hidastaa hyökkääjiä tarkoituksella.
  • Ei formaattiversiota: Ilman headeria/versiota et voi myöhemmin muuttaa iteraatiomäärää, algoritmia tai parametreja ilman, että vanhat tiedot jäävät „hylyksi“.
  • Ei eheystarkistusta: AES-CBC salaa mutta ei estä manipulointia. Ilman autentikointia (esim. HMAC tai AEAD kuten GCM) saat bitinflippaus-/padding-ongelmia ja vaikeasti diagnosoitavia virhetiloja.

Tämän kirjoituksen ydin: pieni konttiformaatti, joka tukee striimausta, on versionhallittavissa ja välttää yleisimmät virheet.

AES-salaus Delphi headerilla, Saltilla, IV:llä ja PBKDF2:lla

Määrittelemme yksinkertaisen konttiformaatin, jota voi käyttää myös tietokanta-BLOBeissa tai viestin payloadeissa:

  • Magic: 4 tavua, esim. NBAE (nopea „Onko tämä meidän formaattimme?“-tarkistus)
  • Versio: 1 tavun (mahdollistaa migraation)
  • KDF-parametrit: iteraatiomäärä (4 tavua)
  • Salt: 16 tavua (satunnainen jokaiselle tiedostolle)
  • IV: 16 tavua (satunnainen jokaiselle tiedostolle AES-CBC:lle)
  • Salausteksti: salatut hyötyaineistot (striimauskelpoinen)

Tärkeää: Salt ja IV eivät ole salaisia. Niiden on vain oltava uusia jokaista salausta kohti. Salasana pysyy salassa; siitä johdettua avainta ei tallenneta.

AES-salaus Delphi streamissa: kontin kirjoitus/luku

Koodi on tietoisesti kirjoitettu „suunnitelmaksi“: selkeästi erotetut funktiot, tarkistettavat headerit, ei piileviä globaaleja. AES:ään ja PBKDF2:een monet tiimit käyttävät vakiintunutta kryptokirjastoa (esim. DEC). Katkelma näyttää formaatin ja striimausmallin; AES-/PBKDF2-kutsut on kapseloitu siten, että voit vaihtaa toteutuksen kirjaston mukaan.

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
// Kryptografiseen satunnaisuuteen: käytä käyttöjärjestelmän CSPRNG:ää (Windows BCryptGenRandom,
// Linux getrandom/urandom). Tässä tarkoituksella paikantaja.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG ei ole liitetty‘);
end;

function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Toteutus esim. DEC (PBKDF2) tai muu kirjasto.
// Tuloksena AKeyLen tavua.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: ei ole liitetty‘);
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Toteuta kirjaston kautta:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Tärkeää: käsittele streamia virta-orientoidusti, älä lataa kaikkea muistiin.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: ei ole liitetty‘);
end;

procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: ei ole liitetty‘);
end;

// — Helper —

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Headera ei voitu kirjoittaa‘);
end;

function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header puutteellinen‘);

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(‚Ei kelvollista Containeria (Magic ei täsmää)‘);

if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Tuntematon Container-versio: %d‘, [H.Version]);

if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Iteraatioiden määrä on epäuskottavien rajojen ulkopuolella‘);

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(‚Salasana ei saa olla tyhjä‘);

// 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(‚Salasana ei saa olla tyhjä‘);

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.

Tarkoitus: Minimipohjainen kontti, joka soveltuu tiedostoille ja BLOB:eille, sisältäen versionhallinnan ja KDF-parametrit. Reunaehdot: Alustan alle on kytkettävä aito CSPRNG-yhteys (kryptografisesti turvallinen satunnaisuus käyttöjärjestelmästä) ja luotettava AES/PBKDF2-implementaatio. Ansat: Älä käytä ”mitä tahansa” satunnaislähdettä (ei Random()), älä käytä kiinteitä IV:itä, ja suunnittele Decryptissä yksiselitteinen virheenkäsittely (väärä salasana vs. vioittuneet tiedot). Vaihtoehdot: CBC:n sijaan mieluummin AEAD (katso alla), tai laajenna headeria algoritmi-ID:llä ja HMACilla.

Eheys: miksi pelkkä AES-CBC on tuotannossa liian riskialtis

AES-CBC on monissa legacy-ympäristöissä yhä käytössä ja voi toimia, jos käytätte lisäksi eheystarkistusta. Ilman eheyttä hyökkääjä voi muokata ciphertextiä; jopa ilman aktiivista hyökkääjää siirtojen virheet tai vialliset tallennuskerrokset voivat aiheuttaa vaikeasti diagnosoitavia „Padding”-virheitä.

Käytännölliset vaihtoehdot:

  • Encrypt-then-HMAC: Kirjoita Ciphertextin jälkeen HMAC (esim. HMAC-SHA-256) Headerin+Ciphertextin yli. Lukuvaiheessa tarkista ensin HMAC, sitten pura. Tätä varten johda mieluiten kaksi avainta PBKDF2:lla (esim. 64 tavua: 32 AES:lle, 32 HMAC:lle), älä käytä samaa avainta kahteen kertaan.
  • AES-GCM: AEAD-tila (Authenticated Encryption with Associated Data). Tuottaa Ciphertextin + Auth-Tagin. Tämä on usein siistein valinta nykyään, jos Delphi-kirjastonne tukee GCM:ää vakaasti. Header-kentät voidaan autentikoida „AAD”:na ilman, että niitä tarvitsee salata.

Jos teidän on pysyttävä CBC:ssä (esim. yhteentoimivuussyistä), Encrypt-then-HMAC on kestävä lisä. Uusissa formaateissa GCM kannattaa, koska se tuo autentikoinnin mukanaan ja virhekuviot ovat selvempiä.

Poikkeuksellisen tärkeää: „kryptografinen satunnaisuus” ja miksi System.Hash ei riitä

Yksi yleinen legacy-refleksi Delphi-projekteissa: „Otetaan vain SHA256 aikaleiman + jonkun muun yli ja saadaan Random.” Tämä ei ole luotettava perusta. Saltia ja IV:ää varten tarvitsette käyttöjärjestelmän tarjoaman CSPRNGin (Cryptographically Secure Pseudo Random Number Generator). Windows-ympäristössä tämä on tyypillisesti BCrypt-API (CNG), Linux-ympäristössä kernelin generaattori kuten getrandom() tai /dev/urandom. Käytännön ero on se, että CSPRNG on suunniteltu siten, että havaituista arvoista ei voi ennustaa tulevia arvoja.

Arkkitehtuurivinkki: Kapseloi tämä pieneen „RandomProvider”-yksikköön, jonka voit mokata testeissä. Näin ratkaiset kaksi reunatapausta kerralla: toistettavat testit (kiinteällä seedillä mokissa) ja todellisen turvallisuuden tuotantoympäristössä (OS-CSPRNG). Estätte tällä, että hotfixissä kukaan ”heittää” taas nopeasti Random()iä, koska se tuntuu nopealta.

Debuggaus ja legacy-migraatio: versionointi ei ole ylellisyyttä

Header ei ole vain „krypto-kauneutta”, vaan ylläpidettävyyttä varten:

  • Iteraatioiden säätö: PBKDF2:n iterointiluvut muuttuvat vuosien aikana. Header-kentän avulla voitte myöhemmin nostaa niitä ilman, että vanhat tiedot muuttuvat lukukelvottomiksi.
  • Formaattimuutokset: Versio 2 voisi esimerkiksi vaihtaa AES-GCM:ään tai lisätä HMACin.
  • Kenttädiagnoosi: Magic/Version mahdollistavat nopeita tarkastuksia lokissa ja työkaluissa ilman, että dataa tarvitsee purkaa.

Käytäntövinkki: Toteuttakaa pieni „Inspector“, joka lukee vain headerin (Magic/Version/Iterations) ja kirjoittaa sen lokiin. Näin selvitätte monet tukitapaukset („Mikä versio tässä on?“) ilman salasanankäsittelyä.

Siisti migraatio: „Read old, write new“ Big Bangin sijaan

Kun korvaatte vanhan formaatin (esim. kiinteä IV, ei KDF:ää, Blowfish/3DES tai itse rakennettu XOR), on Delphi-projekteissa toiminut malli: lukiessa tunnistatte useita formaatteja (Magic/Version tai fallback-heuristiikka), kirjoittaessa tuotatte vain uuden formaatin. Lisäksi onnistuneen salauksenpurun jälkeen voitte taustalla re-encryptata („lazy migration“), jos se sopii prosessiin. Näin pienennätte rollout-riskiä ja vältätte „koko datan kertalukeminen/uuelleen-salaus“ -tyyppiset ylläpitokatkot.

Threading und Streaming: tyypilliset ongelmakohdat Delphi-projekteissa

Salaus ajetaan usein worker-threadseissä (esim. viennissä, uploadissa asiakkaan portaaliin Kundenportal, suurten arkistojen kirjoituksessa). Kaksi kohtaa, jotka nousevat Delphi-projekteissa säännöllisesti esiin:

  • Stream-Positionen: Ennen salaamista/purkamista määritelkää selkeät sopimukset: input-streami luetaan nykyisestä positiosta alkaen, output-streamiä kirjoitetaan nykyisestä positiosta alkaen. Jos streamien uudelleenkäyttö on mahdollista, asettakaa Position := 0 tietoisesti.
  • Memory-Spitzen: Välttäkää „kaikki teratavuina“ -ratkaisuja. Stream-lähestymistapa on tärkeä erityisesti suurille tiedostoille. Jos kryptokirjastonne hyväksyy vain tavutaulukot, kannattaa panostaa stream-kykyiseen toteutukseen tai rakentaa puskuroitu adapteri.

Jos salaatte palveluissa ((Windows- tai Linux-palvelut), kiinnittäkää huomiota myös selkeään poikkeuslokitukseen: „väärä salasana“, „Header korruptoitunut“, „Tag/HMAC virheellinen“ ovat eri käyttötilanteita ja niiden tulee olla eroteltavissa. Tärkeää: virheilmoitukset eivät saa olla ulospäin liian yksityiskohtaisia (ei „padding väärä lohkossa 7“ API-virheeksi), mutta lokissa tiedot saavat olla tarkempia.

Milloin lähestymistapa kannattaa — ja missä se voi epäonnistua

Kannattaa, jos teillä on: (a) pitkäaikaista säilytystä salatuille export-/import-datalle, (b) rinnakkain pyöriviä ohjelmaversioita, (c) datan käsittelyä streameina tai (d) tarve siistille kryptorajapinnalle useille moduuleille (Client/Server/Tooling).

Epäonnistuu, jos yritätte ratkaista tällä „kaiken“: Siirtoon (Transport) vastaa TLS, ei itse rakennettu AES-wrapper. Salaisuuksiin (Secrets) — kuten käyttöoikeussalasanat ja tokenit — sopii usein paremmin käyttöjärjestelmän secret-store tai erillinen Vault. Ja jos tarvitsette interoperabiliteettia muiden kielten kanssa, dokumentoikaa header, endianness ja enkoodaus täsmällisesti (tai käyttäkää vakiintunutta formaattia).

Yhteenveto: AES Delphi-projekteissa on vähemmän algoritmiä, enemmän insinöörityötä

Varsinainen hyöty ei ole pelkästään „AES toimii“, vaan käyttökelpoinen formaatti: satunnainen suola (salt) ja IV, versioitu header, PBKDF2-parametrit payloadissa ja virtauskäsittely. Täydennä uusille formaateille mahdollisuuksien mukaan eheysmekanismi (AES-GCM tai Encrypt-then-HMAC). Näin „me salataan jotain“ muuttuu komponentiksi, joka on digitaalisten yritysjärjestelmien osana ylläpidettävissä ja migroitavissa myös vuosien jälkeen.

Jos sinun täytyy integroida tällainen container vakiintuneeseen Delphi-ympäristöön tai migroida se siististi legacy-muodosta, kannattaa tehdä lyhyt arkkitehtuurintarkastus (avainhallinta, formaattiversiot, käyttö/lokitus). Yksityiskohdat sovimme mielellämme tarpeen mukaan keskustelussa:

Toiminnallisessa ympäristössä myös Delphi Aes ja Pbkdf2 Delphi näyttelevät tärkeää roolia, kun integraatioiden, tietovirtojen ja jatkokehityksen on toimittava yhdessä hallitusti.

Keskustele projektista tai modernisointihankkeesta Net-Base kanssa.

Seuraava vaihe

Kun aiheesta tulee todellinen projekti, arkkitehtuuri, nykyinen järjestelmäkanta ja käyttö tulisi varhaisessa vaiheessa tarkastella yhdessä.

Emme tue pelkästään yksittäiskysymyksissä, vaan myös silloin, kun lähdekoodipalasista, legacy-aiheista tai portaali-ideoista halutaan muodostaa luotettava yrityshanke.

  • Nykytila, tavoitetila ja tekniset riskit arvioidaan yhdessä.
  • REST, datan käyttö, portaalit ja käyttöönotto eivät jätetä myöhempien seurausten varaan.
  • Näette ajoissa, mikä ratkaisu on taloudellisesti ja toiminnallisesti kestävä.

Jaa artikkeli

Jaa tämä viesti suoraan

LinkedIn, X, XING, Facebook, WhatsApp ja sähköposti ovat heti käytettävissä. Instagramia varten valmistelemme linkin ja lyhyen tekstin.

Sähköposti

Instagram avautuu uuteen välilehteen. Linkki ja lyhyt teksti kopioidaan ensin leikepöydälle.