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 := 0tietoisesti. - 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ä.