Fra magasinets tema til projektpraksis
Passende service- og tekniske sider til artiklen
Ved AES-kryptering Delphi fejler det i praksis sjældent på „AES i sig selv“, men på randbetingelser: data skal behandles som stream (filer, BLOBs, backups), gamle formater skal forblive læsbare, og i drift har man brug for fejlsøgningsmuligheder (header, versionering) og sikre standardindstillinger (salt/IV tilfældigt, ingen genbrug). Dette kildekodesnippet viser derfor ikke kun „Encrypt/Decrypt“, men et lille, robust format med header, version, salt og IV – plus PBKDF2 til nøgleafledning og et sted, hvor integritet fornuftigt kan tilføjes.
Hvorfor „AES-String verschlüsseln“ næsten aldrig er nok
I individuel virksomhedssoftware optræder kryptering typisk tre steder: (1) konfiguration/secrets (fx adgangsoplysninger), (2) udvekslings-/eksportfiler og (3) hvilende data (fx arkiver, dokumentcontainere). Den naive tilgang „adgangskode → AES-nøgle → streng ind/ud“ falder hurtigt sammen:
- Genbrug af IV: I tilstande som CBC eller GCM skal en initialiseringsvektor (IV) være unik for hver kryptering. Et konstant IV er en lækage, selv hvis adgangskoden er stærk.
- Nøgle fra adgangskode uden KDF: At bruge en adgangskode direkte som nøgle (eller hashe den én gang) inviterer offline-angreb. En KDF (Key Derivation Function) som PBKDF2 sinker angribere målrettet.
- Ingen formatversion: Uden header/version er det svært senere at ændre iterationsantal, algoritme eller parametre uden at gøre ældre data uanvendelige.
- Ingen integritet: AES-CBC krypterer, men forhindrer ikke manipulation. Uden autentificering (f.eks. HMAC eller AEAD som GCM) får man bitflipping-/padding-problemer og vanskeligt diagnosticerbare fejltilstande.
Kernen i dette indlæg: et lille containerformat, der understøtter streaming, kan versioneres og undgår almindelige fejl.
AES-kryptering Delphi med header, salt, IV og PBKDF2
Vi definerer et simpelt containerformat, som også kan bruges i database-BLOBs eller message-payloads:
- Magic: 4 Bytes, fx
NBAE(hurtig „Er det vores format?“-kontrol) - Version: 1 Byte (muliggør migration)
- KDF-Parameter: iterationsantal (4 Bytes)
- Salt: 16 Bytes (tilfældigt per fil)
- IV: 16 Bytes (tilfældigt per fil for AES-CBC)
- Ciphertext: krypterede nyttedata (streaming-kompatibelt)
Vigtigt: Salt og IV er ikke hemmelige. De skal blot være nye for hver kryptering. Adgangskoden forbliver hemmelig; den deraf afledte nøgle gemmes ikke.
AES-kryptering Delphi i stream: Container schreiben/lesen
Koden er bevidst skrevet som en „byggeplan“: klart adskilte funktioner, verificerbare header, ingen skjulte globale variable. Til AES og PBKDF2 bruger mange teams et veldokumenteret crypto-bibliotek (fx DEC). Snipppet viser formatet og streaming-mønstret; AES-/PBKDF2-kald er kapslet, så I kan udskifte dem afhængigt af bibliotek.
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
// Til kryptografisk tilfældighed: brug OS-CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Her med vilje som pladsholder.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG ikke tilgængelig‘);
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementering f.eks. med DEC (PBKDF2) eller et andet bibliotek.
// Resultat: AKeyLen bytes.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: ikke tilgængelig‘);
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementering via bibliotek:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Vigtigt: Behandl data stream-orienteret, ikke alt i hukommelsen.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: ikke tilgængelig‘);
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: ikke tilgængelig‘);
end;
// — Helper —
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header kunne ikke skrives‘);
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header ufuldstændig‘);
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(‚Ikke en gyldig container (Magic stemmer ikke)‘);
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Ukendt containerversion: %d‘, [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Antallet af iterationer uden for plausible grænser‘);
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(‚Adgangskode må ikke være tom‘);
// 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(‚Adgangskode må ikke være tom‘);
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.
Formål: En minimal container, der egner sig til filer og BLOBs, inklusiv versionsstyring og KDF-parametre. Randbetingelser: Der skal være en ægte CSPRNG-tilslutning (kryptografisk sikker tilfældighed fra operativsystemet) og en robust AES/PBKDF2-implementering i underlaget. Faldgruber: Tag ikke bare „en eller anden“ tilfældighed (ingen Random()), ingen faste IVs, og planlæg entydig fejlbehandling ved decrypt (forkert password vs. beskadigede data). Varianter: brug hellere AEAD i stedet for CBC (se nedenfor), eller udvid header med algoritme-ID og HMAC.
Integritet: hvorfor AES-CBC alene i drift er for risikabelt
AES-CBC findes stadig i mange legacy-kontekster og kan fungere, hvis du yderligere indfører en integritetsbeskyttelse. Uden integritet kan en angriber manipulere Ciphertext; også uden aktiv angriber kan transmissionsfejl eller defekte storage-lag skabe svære at diagnosticere „padding“-fejl.
Pragmatiske muligheder:
- Encrypt-then-HMAC: Efter Ciphertext skriver du et HMAC (f.eks. HMAC-SHA-256) over Header+Ciphertext. Ved læsning kontrolleres HMAC først, derefter dekrypteres. Til det formål udleder du ideelt set to nøgler fra PBKDF2 (f.eks. 64 Bytes: 32 til AES, 32 til HMAC), i stedet for at genbruge den samme nøgle to gange.
- AES-GCM: AEAD-mode (Authenticated Encryption with Associated Data). Leverer Ciphertext + Auth-Tag. Det er i dag ofte det reneste valg, hvis din Delphi-bibliotek understøtter GCM stabilt. Header-felter kan autentificeres som „AAD“ uden at du behøver kryptere dem.
Hvis du er nødt til at blive ved CBC (f.eks. af interoperabilitetsgrunde), er Encrypt-then-HMAC den robuste tilføjelse. For nye formater er GCM ofte værd at vælge, fordi du dermed får autentifikation „med“ og fejlbilleder bliver tydeligere.
Usædvanligt vigtigt: „kryptografisk tilfældighed“ og hvorfor System.Hash ikke er nok
Et almindeligt legacy-refleks i Delphi-projekter: „Vi tager bare SHA256 over tidsstempel + noget og så har vi Random.“ Det er ikke et pålideligt grundlag. Til salt og IV har du brug for en CSPRNG (Cryptographically Secure Pseudo Random Number Generator) fra operativsystemet. Under Windows er det typisk BCrypt-API’en (CNG), under Linux en kernel-generator som getrandom() eller /dev/urandom. Forskellen er praktisk: En CSPRNG er designet, så observerede værdier ikke gør det muligt at forudsige efterfølgende værdier.
Arkitektur-trick: Indkapsl det i en lille „RandomProvider“-unit, som du kan mocke i tests. Dermed løser du to tilfælde på én gang: reproducerbare tests (med fast seed i mock) og rigtig sikkerhed i produktion (med OS-CSPRNG). På den måde forhindrer du, at en hurtig hotfix igen trækker Random() ind, fordi det er nemt.
Debugging og legacy-migrering: Versionsstyring er ikke luksus
Headeren er ikke kun for „krypto-æstetik“, men for vedligeholdelse:
- Iteration Tuning: PBKDF2-iterationsantallet ændrer sig over årene. Med et header-felt kan du senere øge det uden at gøre gamle data ulæselige.
- Formatwechsel: Version 2 kunne f.eks. skifte til AES-GCM eller tilføje et HMAC.
- Diagnose im Feld: Magic/Version tillader hurtige checks i logs og værktøjer uden at dekryptere data.
Praktisk tip: Implementer en lille „Inspector“, der kun læser headeren (Magic/Version/Iterations) og skriver den i en log. Dermed afklares mange supporttilfælde („Hvilken version er dette?“) uden password-håndtering.
Rydelig migration: „Read old, write new“ i stedet for Big Bang
Hvis du afløser et gammelt format (fx fast IV, ingen KDF, Blowfish/3DES eller hjemmelavet XOR), har et mønster vist sig effektivt i Delphi-projekter: Ved læsning genkender du flere formater (Magic/Version eller fallback-heuristik), ved skrivning producerer du kun det nye format. Derudover kan du ved succesfuld dekryptering genkryptere i baggrunden (»lazy migration«), hvis det passer ind i processen. Så reducerer du rollout-risiko og undgår at skulle «kryptere alt på ny» som et vedligeholdelsesvindue.
Threading og streaming: typiske kanttilfælde i Delphi
Kryptering kører ofte i worker-threads (fx ved eksport, ved upload til et kundeportal, ved skrivning af store arkiver). To punkter, som ofte dukker op i Delphi-projekter:
- Stream-positioner: Inden kryptering/dekryptering klare kontrakter: Input-stream læses fra aktuell position, output-stream skrives fra aktuell position. Ved genbrug af streams skal du altid eksplicit sætte
Position := 0. - Hukommelsestoppe: Undgå at læsse «alt i TBytes». Stream-tilgangen er især vigtig for store filer. Hvis din crypto-bibliotek kun accepterer byte-arrays, er det værd at investere i en stream-kompatibel implementation eller bygge en buffret adapter.
Hvis du krypterer i services (Windows- eller Linux-Services), skal du desuden sikre klart exception-logging: „forkert adgangskode“, „Header beskadiget“, „Tag/HMAC ugyldig“ er forskellige driftsforhold og bør kunne skilles ad. Vigtigt: Fejlbeskeder må ikke være for detaljerede udadtil (ingen „Forkert padding i blok 7“ som API-fejl), men internt i loggen gerne mere præcise oplysninger.
Hvornår tilgangen er fordelagtig – og hvor den kan fejle
Fordelagtig, når du: (a) gemmer krypterede eksport-/importdata med lang levetid, (b) kører flere programversioner parallelt, (c) behandler data som streams, eller (d) har brug for en konsistent krypto-grænseflade for flere moduler (Client/Server/Tooling).
Fejler, når du prøver at løse „alt“: Til transport hører TLS, ikke en hjemmelavet AES-wrapper. For secrets (adgangskoder, tokens) er et OS-Secret-Store eller en Vault ofte mere passende. Og hvis du skal interopere med andre sprog, skal du dokumentere header, endianness og encoding præcist (eller bruge et etableret format).
Konklusion: AES i Delphi er mindre algoritme, mere engineering
Den egentlige gevinst ved dette uddrag er ikke „AES virker“, men et driftsklart format: tilfældig salt og IV, versioneret header, PBKDF2-parametre i payload og streambaseret behandling. Suppler for nye formater helst med integritet (AES-GCM eller Encrypt-then-HMAC). Så bliver det fra „vi krypterer noget“ til en byggesten, som i digitale virksomhedsløsninger stadig er vedligeholdelsesvenlig og migrerbar efter år.
Når en sådan container skal integreres i et etableret Delphi-landskab eller migreres fra et legacy-format på en ren måde, er et kort arkitekturcheck (nøglehåndtering, formatversioner, drift/logning) værd at overveje. Detaljer afklarer vi gerne i en samtale efter behov:
I det faglige miljø spiller også Delphi Aes og Pbkdf2 Delphi en vigtig rolle, når integrationer, dataflows og videreudvikling skal fungere gnidningsfrit sammen.
Næste trin
Når et emne bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.
Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.
- Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
- REST, dataadgang, portaler og idrulning bliver ikke udskudt som eftertanker.
- I ser tidligt, hvilken vej der er økonomisk og driftsmæssigt holdbar.