Net-Base Magazine

30.05.2026

AES-versleuteling in Delphi: een robuust broncodefragment met IV, salt, header en streaming

Een praktisch toepasbaar Delphi-sourcefragment voor AES-versleuteling met willekeurige salt en IV, een duidelijke bestandsheaderstructuur, PBKDF2-sleutelafleiding en streaming – inclusief typische valkuilen bij legacy-formaten, integriteit en operationeel beheer.

30.05.2026

Van magazinethema naar projectpraktijk

Relevante dienst- en technische pagina's bij het artikel

Bij AES-versleuteling Delphi faalt het in de praktijk zelden aan „AES zelf“, maar aan randvoorwaarden: gegevens moeten als Stream verwerkt worden (bestanden, BLOBs, Backups), oude formaten moeten blijvend leesbaar blijven, en in productie heeft u debugbaarheid nodig (Header, Versionierung) en veilige defaults (Salt/IV willekeurig, geen hergebruik). Dit Source-Schnipsel toont daarom niet alleen „Encrypt/Decrypt“, maar een klein, belastbaar formaat met Header, Version, Salt en IV – plus PBKDF2 voor de sleutelafleiding en een plek waar integriteit zinvol kan worden aangevuld.

Waarom „AES-String verschlüsseln“ bijna nooit volstaat

In maatwerk bedrijfssoftware komt versleuteling typisch op drie plekken voor: (1) Konfiguration/Secrets (bijv. toeganggegevens), (2) Austausch-/Exportdateien en (3) ruhende Daten (bijv. archieven, documentcontainers). De naïeve aanpak „Passwort → AES-Key → String rein/raus“ faalt daarbij snel:

  • IV-Wiederverwendung: Bij modi zoals CBC of GCM moet een Initialisierungsvektor (IV) per versleuteling uniek zijn. Een constant IV is een lek, ook als het wachtwoord sterk is.
  • Key aus Passwort ohne KDF: Een wachtwoord direct als sleutel gebruiken (of eenmalig hashen) nodigt offline-aanvallen uit. Een KDF (Key Derivation Function) zoals PBKDF2 vertraagt aanvallers doelgericht.
  • Keine Formatversion: Zonder Header/Version kunt u het aantal iteraties, het algoritme of parameters later nauwelijks wijzigen zonder dat bestaande gegevens onbruikbaar worden.
  • Keine Integrität: AES-CBC versleutelt, maar voorkomt geen manipulatie. Zonder authenticatie (bijv. HMAC of AEAD zoals GCM) krijgt u Bitflipping-/Padding-problemen en moeilijk te diagnosticeren foutbeelden.

De kern van dit artikel: een klein containerformaat dat Streaming ondersteunt, versioneerbaar is en de standaardfouten vermijdt.

AES Verschlüsselung Delphi met Header, Salt, IV und PBKDF2

We definiëren een simpel containerformaat dat zich ook in Datenbank-BLOBs of Message-Payloads laten gebruiken:

  • Magic: 4 Bytes, bijv. NBAE (snelle „Ist das unser Format?“-check)
  • Version: 1 Byte (maakt migratie mogelijk)
  • KDF-Parameter: Iterationszahl (4 Bytes)
  • Salt: 16 Bytes (willekeurig per Datei)
  • IV: 16 Bytes (willekeurig per Datei voor AES-CBC)
  • Ciphertext: verschlüsselte Nutzdaten (streamingfähig)

Belangrijk: Salt en IV zijn niet geheim. Ze hoeven alleen per versleuteling nieuw te zijn. Het Passwort blijft geheim; de daaruit afgeleide Key wordt niet opgeslagen.

AES Verschlüsselung Delphi im Stream: Container schreiben/lesen

De code is bewust als „Bauplan“ geschreven: duidelijk gescheiden functies, prüfbare Header, geen Hidden-Globals. Voor AES en PBKDF2 gebruiken veel teams een beproefde Crypto-Bibliothek (bijv. DEC). Het Schnipsel toont het formaat en het Streaming-Pattern; de AES-/PBKDF2-aanroepen zijn zo kapselbaar dat u ze afhankelijk van de Bibliothek kunt uitwisselen.

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;

// — Afhankelijkheden die u, afhankelijk van de crypto-stack, moet implementeren —

procedure FillRandomBytes(var B: TBytes);
begin
// Voor cryptografische willekeur: gebruik de OS-CSPRNG (bijv. Windows BCryptGenRandom,
// Linux getrandom/urandom). Hier opzettelijk als plaatsaanduiding.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG niet gekoppeld‘);
end;

function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementatie bijv. met DEC (PBKDF2) of een andere bibliotheek.
// Resultaat: AKeyLen bytes.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: niet gekoppeld‘);
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementatie via bibliotheek:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Belangrijk: stream-georiënteerd verwerken, niet alles in geheugen.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: niet gekoppeld‘);
end;

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

// — Hulpfuncties —

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header kon niet geschreven worden‘);
end;

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

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(‚Geen geldige container (Magic klopt niet)‘);

if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Onbekende containerversie: %d‘, [H.Version]);

if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Aantal iteraties valt buiten plausibele grenzen‘);

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(‚Wachtwoord mag niet leeg zijn‘);

// Salt/IV genereren
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);

// Header vullen
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);

// Sleutel afleiden (32 bytes voor AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);

// Nuttige data versleutelen (ciphertext volgt direct na 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(‚Wachtwoord mag niet leeg zijn‘);

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);

// Ontsleutelen vanaf de huidige stream-positie (na 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.

Doel: Een minimale container die geschikt is voor bestanden en BLOBs, inclusief versiebeheer en KDF-parameters. Randvoorwaarden: U moet een echte CSPRNG-koppeling (cryptografisch veilige willekeur uit het besturingssysteem) en een robuuste AES/PBKDF2-implementatie daaronder leggen. Valkuilen: Gebruik niet zomaar willekeur (geen Random()), geen vaste IVs, en plan bij het decrypten een eenduidige foutafhandeling (verkeerd wachtwoord versus beschadigde data). Varianten: in plaats van CBC liever AEAD (zie hieronder), of de header uitbreiden met algoritme-ID en HMAC.

Integriteit: waarom AES-CBC alleen in productie te risicovol is

AES-CBC komt in veel legacy-contexten nog voor en kan werken als u aanvullend voor integriteitscontrole zorgt. Zonder integriteit kan een aanvaller de ciphertext manipuleren; ook zonder actieve aanvaller veroorzaken transmissiefouten of beschadigde opslaglagen lastig te diagnosticeren „padding“-fouten.

Pragmatische opties:

  • Encrypt-then-HMAC: Schrijf een HMAC (bijv. HMAC-SHA-256) over header+ciphertext achter de ciphertext. Bij het lezen eerst de HMAC controleren, daarna ontsleutelen. Hiervoor leidt u idealiter twee keys uit PBKDF2 af (bijv. 64 bytes: 32 voor AES, 32 voor HMAC), in plaats van dezelfde key dubbel te gebruiken.
  • AES-GCM: AEAD-modus (Authenticated Encryption with Associated Data). Levert ciphertext + auth-tag. Dit is tegenwoordig vaak de netste keuze als uw Delphi-bibliotheek GCM stabiel ondersteunt. Headervelden kunnen als „AAD“ geauthenticeerd worden, zonder dat u ze hoeft te versleutelen.

Als u bij CBC moet blijven (bijv. om interoperabiliteit), is Encrypt-then-HMAC de robuuste aanvulling. Voor nieuwe formaten verdient GCM de voorkeur, omdat u daarmee authenticatie „mee“ krijgt en foutbeelden duidelijker worden.

Uitzonderlijk belangrijk: „cryptografische willekeur“ en waarom System.Hash niet volstaat

Een veelvoorkomende legacy-reflex in Delphi-projecten: „We nemen gewoon SHA256 over timestamp + wat dan ook en hebben Random.“ Dat is geen betrouwbare basis. Voor salt en IV heeft u een CSPRNG (Cryptographically Secure Pseudo Random Number Generator) van het besturingssysteem nodig. Onder Windows is dat typisch de BCrypt-API (CNG), onder Linux een kernel-generator zoals getrandom() respectievelijk /dev/urandom. Het praktische verschil: een CSPRNG is ontworpen zodat uit waargenomen waarden geen voorspelling van volgende waarden mogelijk is.

Architectuur-tip: kapsel dit in een kleine „RandomProvider“-unit die u in tests kunt mocken. Daarmee lost u direct twee randgevallen op: reproduceerbare tests (met vaste seed in de mock) en echte veiligheid in productie (met OS-CSPRNG). Zo voorkomt u dat in een hotfix weer „even snel“ Random() wordt ingevoerd omdat het snel werkt.

Debugging en legacy-migratie: versiebeheer is geen luxe

De header is niet alleen voor „crypto-schoonheid“, maar voor onderhoudbaarheid:

  • Iteratie-tuning: PBKDF2-iteratieaantallen veranderen in de loop der jaren. Met een headerveld kunt u later opschroeven zonder oude data onleesbaar te maken.
  • Formatwissel: Versie 2 kan bijvoorbeeld overstappen op AES-GCM of een HMAC toevoegen.
  • Diagnose in het veld: Magic/Version maken snelle controles in logs en tools mogelijk, zonder de data te ontsleutelen.

Praktijktip: Implementeer een kleine „inspector“ die alleen de header uitleest (Magic/Version/Iterations) en in een log schrijft. Hiermee lost u veel supportgevallen op („Welke versie ligt hier?“) zonder wachtwoordverwerking.

Zorgvuldig migreren: „Read old, write new“ in plaats van Big Bang

Als u een oud formaat vervangt (bijv. vaste IV, geen KDF, Blowfish/3DES of zelfgemaakte XOR), heeft zich in Delphi-projecten een patroon bewezen: bij het lezen herkent u meerdere formaten (Magic/Version of fallback-heuristiek), bij het schrijven genereert u alleen nog het nieuwe formaat. Daarnaast kunt u na een succesvolle ontsleuteling op de achtergrond re-encrypten („lazy migration“), als dat in het proces past. Zo verkleint u het rollout-risico en voorkomt u een onderhoudsvenster waarin alles opnieuw moet worden versleuteld.

Threading en streaming: typische knelpunten in Delphi

Versleuteling loopt vaak in worker-threads (bijv. bij export, bij upload naar een klantenportaal, bij het wegschrijven van grote archieven). Twee punten die in Delphi-projecten regelmatig opvallen:

  • Stream-posities: Stel vóór het versleutelen/ontsleutelen duidelijke contracts op: de input-stream wordt gelezen vanaf de huidige positie, de output-stream wordt beschreven vanaf de huidige positie. Bij hergebruik van streams moet u bewust Position := 0 zetten.
  • Geheugenpieken: Vermijd „alles in TBytes“. De stream-aanpak is juist bij grote bestanden essentieel. Als uw crypto-bibliotheek alleen byte-arrays accepteert, is de extra inspanning de moeite waard om naar een stream-geschikte implementatie te gaan of een gebufferde adapter te bouwen.

Als u in services versleutelt (Windows- of Linux-services), let dan ook op degelijk exception-logging: „verkeerd wachtwoord“, „header kapot“, „Tag/HMAC ongeldig“ zijn verschillende bedrijfsgevallen en moeten van elkaar te onderscheiden zijn. Belangrijk: foutmeldingen mogen extern niet te gedetailleerd zijn (geen „Padding fout in block 7“ als API-fout), intern in de logs mogen ze dat wel zijn.

Wanneer de aanpak de moeite waard is – en waar ze kan kantelen

De moeite waard als u: (a) versleutelde export-/importgegevens langdurig opslaat, (b) verschillende programmaversies parallel draait, (c) data als streams verwerkt of (d) een schone crypto-interface voor meerdere modules (client/server/tooling) nodig heeft.

Kantelt als u probeert er „alles“ mee op te lossen: voor transport is TLS bedoeld, niet een zelfgemaakt AES-wrapper. Voor secrets (wachtwoorden, tokens) is vaak een OS-Secret-Store of een Vault zinniger. En als u interoperabiliteit met andere talen nodig heeft, moet u header, endianness en encoding exact documenteren (of een gevestigd formaat gebruiken).

Conclusie: AES in Delphi is minder algoritme, meer engineering

De werkelijke winst van dit stukje is niet „AES werkt“, maar een operationeel inzetbaar formaat: willekeurige salt en IV, een geversioneerde header, PBKDF2-parameters in de payload en stream-geschikte verwerking. Breid voor nieuwe formaten bij voorkeur integriteit toe (AES-GCM of Encrypt-then-HMAC). Zo verandert „we versleutelen maar iets“ in een bouwsteen die in digitale bedrijfsoplossingen ook na jaren nog onderhoudbaar en migreerbaar is.

Als u zo’n container in een gegroeid Delphi-landschap moet integreren of netjes van een Legacy-Format moet migreren, is een korte architectuur-check de moeite waard (sleutelbeheer, formatversies, operatie/logging). Details bespreken we desgewenst graag in een gesprek:

In de vakinhoudelijke context spelen ook Delphi Aes en Pbkdf2 Delphi een belangrijke rol, wanneer integraties, datastromen en doorontwikkeling netjes op elkaar moeten aansluiten.

Project of moderniseringsproject met Net-Base bespreken.

Volgende stap

Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.

We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.

  • Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
  • REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
  • U ziet vroeg welke weg economisch en operationeel houdbaar is.

Bericht delen

Dit bericht direct delen

LinkedIn, X, XING, Facebook, WhatsApp en e-mail zijn direct beschikbaar. Voor Instagram bereiden we de link en een korte tekst direct voor.

E-mail

Instagram opent in een nieuw tabblad. Link en korte tekst worden van tevoren naar het klembord gekopieerd.