Net-Base Magazin

30.05.2026

AES-Verschlüsselung in Delphi: ein robuster Source-Schnipsel mit IV, Salt, Header und Streaming

Ein praxistauglicher Delphi-Source-Schnipsel für AES-Verschlüsselung mit zufälligem Salt und IV, klarer Datei-Header-Struktur, PBKDF2-Key-Derivation und Streaming – inklusive typischer Stolperfallen bei Legacy-Formaten, Integrität und Betrieb.

30.05.2026

Vom Magazinthema zur Projektpraxis

Passende Leistungs- und Technikseiten zum Beitrag

Bei AES Verschlüsselung Delphi scheitert es in der Praxis selten an „AES an sich“, sondern an Randbedingungen: Daten müssen als Stream verarbeitet werden (Dateien, BLOBs, Backups), alte Formate müssen weiter lesbar bleiben, und im Betrieb braucht man Debugbarkeit (Header, Versionierung) und sichere Defaults (Salt/IV zufällig, keine Wiederverwendung). Dieser Source-Schnipsel zeigt daher nicht nur „Encrypt/Decrypt“, sondern ein kleines, belastbares Format mit Header, Version, Salt und IV – plus PBKDF2 für die Schlüsselableitung und eine Stelle, an der Integrität sinnvoll ergänzt wird.

Warum „AES-String verschlüsseln“ fast nie reicht

In individueller Unternehmenssoftware taucht Verschlüsselung typischerweise an drei Stellen auf: (1) Konfiguration/Secrets (z. B. Zugangsdaten), (2) Austausch-/Exportdateien und (3) ruhende Daten (z. B. Archive, Dokumentcontainer). Der naive Ansatz „Passwort → AES-Key → String rein/raus“ kippt dabei schnell:

  • IV-Wiederverwendung: Bei Modi wie CBC oder GCM muss ein Initialisierungsvektor (IV) pro Verschlüsselung einzigartig sein. Ein konstantes IV ist ein Leck, auch wenn das Passwort stark ist.
  • Key aus Passwort ohne KDF: Ein Passwort direkt als Key zu verwenden (oder einmalig zu hashen) lädt Offline-Angriffe ein. Eine KDF (Key Derivation Function) wie PBKDF2 bremst Angreifer gezielt.
  • Keine Formatversion: Ohne Header/Version können Sie Iterationszahl, Algorithmus oder Parameter später kaum ändern, ohne Alt-Daten zu „verwaisen“.
  • Keine Integrität: AES-CBC verschlüsselt, aber verhindert keine Manipulation. Ohne Authentifizierung (z. B. HMAC oder AEAD wie GCM) bekommen Sie Bitflipping-/Padding-Probleme und schwer diagnosbare Fehlerbilder.

Der Kern dieses Beitrags: ein kleines Containerformat, das Streaming unterstützt, versionierbar ist und die Standardfehler vermeidet.

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

Wir definieren ein simples Containerformat, das sich auch in Datenbank-BLOBs oder Message-Payloads nutzen lässt:

  • Magic: 4 Bytes, z. B. NBAE (schneller „Ist das unser Format?“-Check)
  • Version: 1 Byte (ermöglicht Migration)
  • KDF-Parameter: Iterationszahl (4 Bytes)
  • Salt: 16 Bytes (zufällig pro Datei)
  • IV: 16 Bytes (zufällig pro Datei für AES-CBC)
  • Ciphertext: verschlüsselte Nutzdaten (streamingfähig)

Wichtig: Salt und IV sind nicht geheim. Sie müssen nur pro Verschlüsselung neu sein. Das Passwort bleibt geheim; der daraus abgeleitete Key wird nicht gespeichert.

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

Der Code ist bewusst als „Bauplan“ geschrieben: klar getrennte Funktionen, prüfbare Header, keine Hidden-Globals. Für AES und PBKDF2 nutzen viele Teams eine bewährte Crypto-Bibliothek (z. B. DEC). Der Schnipsel zeigt das Format und das Streaming-Pattern; die AES-/PBKDF2-Aufrufe sind so kapselbar, dass Sie sie je nach Bibliothek austauschen können.

Delphi
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 nicht angebunden');
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: nicht angebunden');
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: nicht angebunden');
end;

procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
  raise ENbCryptoError.Create('AES256_CBC_DecryptStream: nicht angebunden');
end;

// --- Helper ---

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
  if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
    raise ENbCryptoError.Create('Header konnte nicht geschrieben werden');
end;

function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
  H: TNbHeaderV1;
begin
  if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
    raise ENbCryptoError.Create('Header unvollstä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('Kein gültiger Container (Magic stimmt nicht)');

  if H.Version <> CVersion then
    raise ENbCryptoError.CreateFmt('Unbekannte Container-Version: %d', [H.Version]);

  if (H.Iterations < 10000) or (H.Iterations > 5000000) then
    raise ENbCryptoError.Create('Iterationszahl außerhalb plausibler 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('Passwort darf nicht leer sein');

  // 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('Passwort darf nicht leer sein');

  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.

Zweck: Ein minimaler Container, der sich für Dateien und BLOBs eignet, inklusive Versionierung und KDF-Parametern. Randbedingungen: Sie müssen eine echte CSPRNG-Anbindung (kryptografisch sicherer Zufall aus dem Betriebssystem) und eine robuste AES/PBKDF2-Implementierung darunter legen. Stolperfallen: Nicht „irgendeinen“ Random nehmen (kein Random()), keine festen IVs, und beim Decrypt eindeutige Fehlerbehandlung (falsches Passwort vs. beschädigte Daten) einplanen. Varianten: statt CBC lieber AEAD (siehe unten), oder Header um Algorithmus-ID und HMAC erweitern.

Integrität: warum AES-CBC allein im Betrieb zu riskant ist

AES-CBC ist in vielen Legacy-Kontexten noch präsent und kann funktionieren, wenn Sie zusätzlich eine Integritätssicherung nutzen. Ohne Integrität kann ein Angreifer Ciphertext manipulieren; auch ohne aktiven Angreifer erzeugen Übertragungsfehler oder defekte Storage-Schichten schwer zu diagnostizierende „Padding“-Fehler.

Pragmatische Optionen:

  • Encrypt-then-HMAC: Nach dem Ciphertext einen HMAC (z. B. HMAC-SHA-256) über Header+Ciphertext schreiben. Beim Lesen zuerst HMAC prüfen, dann entschlüsseln. Dafür leiten Sie idealerweise zwei Keys aus PBKDF2 ab (z. B. 64 Bytes: 32 für AES, 32 für HMAC), statt denselben Key doppelt zu nutzen.
  • AES-GCM: AEAD-Modus (Authenticated Encryption with Associated Data). Liefert Ciphertext + Auth-Tag. Das ist heute oft die sauberste Wahl, wenn Ihre Delphi-Bibliothek GCM stabil unterstützt. Header-Felder können als „AAD“ mit authentifiziert werden, ohne dass Sie sie verschlüsseln müssen.

Wenn Sie bei CBC bleiben müssen (z. B. wegen Interop), ist Encrypt-then-HMAC die robuste Ergänzung. Für neue Formate lohnt sich GCM, weil Sie damit Authentifizierung „mitbekommen“ und Fehlerbilder klarer werden.

Ungewöhnlich wichtig: „Kryptografischer Zufall“ und warum System.Hash nicht reicht

Ein häufiger Legacy-Reflex in Delphi-Projekten: „Wir nehmen einfach SHA256 über Zeitstempel + irgendwas und haben Random.“ Das ist keine verlässliche Grundlage. Für Salt und IV brauchen Sie einen CSPRNG (Cryptographically Secure Pseudo Random Number Generator) des Betriebssystems. Unter Windows ist das typischerweise die BCrypt-API (CNG), unter Linux ein Kernel-Generator wie getrandom() bzw. /dev/urandom. Der Unterschied ist praktisch: Ein CSPRNG ist so entworfen, dass aus beobachteten Werten keine Vorhersage weiterer Werte möglich ist.

Architektur-Kniff: Kapseln Sie das in eine kleine „RandomProvider“-Unit, die Sie in Tests mocken können. Damit lösen Sie gleich zwei Randfälle: reproduzierbare Tests (mit festem Seed im Mock) und echte Sicherheit im Produktivbetrieb (mit OS-CSPRNG). So verhindern Sie, dass in einem Hotfix „mal eben“ wieder Random() einzieht, weil es gerade schnell geht.

Debugging und Legacy-Migration: Versionierung ist kein Luxus

Der Header ist nicht nur für „Krypto-Schönheit“, sondern für Wartbarkeit:

  • Iteration Tuning: PBKDF2-Iterationszahlen ändern sich über die Jahre. Mit Header-Feld können Sie später hochdrehen, ohne alte Daten unlesbar zu machen.
  • Formatwechsel: Version 2 könnte z. B. auf AES-GCM wechseln oder einen HMAC ergänzen.
  • Diagnose im Feld: Magic/Version erlauben schnelle Checks in Logs und Tools, ohne Daten zu entschlüsseln.

Praxis-Tipp: Implementieren Sie einen kleinen „Inspector“, der nur den Header ausliest (Magic/Version/Iterations) und in ein Log schreibt. Damit klären Sie viele Supportfälle („Welche Version liegt hier?“) ohne Passwort-Handling.

Sauber migrieren: „Read old, write new“ statt Big Bang

Wenn Sie ein altes Format ablösen (z. B. festes IV, keine KDF, Blowfish/3DES, oder selbstgebautes XOR), hat sich in Delphi-Projekten ein Muster bewährt: Beim Lesen erkennen Sie mehrere Formate (Magic/Version oder Fallback-Heuristik), beim Schreiben erzeugen Sie nur noch das neue Format. Zusätzlich können Sie beim erfolgreichen Entschlüsseln im Hintergrund re-encrypten („lazy migration“), wenn das zum Prozess passt. So reduzieren Sie Rollout-Risiko und vermeiden „einmal alles neu verschlüsseln“ als Wartungsfenster.

Threading und Streaming: typische Kanten in Delphi

Verschlüsselung läuft oft in Worker-Threads (z. B. beim Export, beim Upload in ein Kundenportal, beim Schreiben großer Archive). Zwei Punkte, die in Delphi-Projekten regelmäßig auffallen:

  • Stream-Positionen: Vor dem Verschlüsseln/Entschlüsseln klare Contracts: Der Input-Stream wird ab aktueller Position gelesen, der Output-Stream ab aktueller Position beschrieben. Bei Wiederverwendung von Streams unbedingt Position := 0 bewusst setzen.
  • Memory-Spitzen: Vermeiden Sie „alles in TBytes“. Der Stream-Ansatz ist gerade für große Dateien wichtig. Falls Ihre Crypto-Bibliothek nur Byte-Arrays akzeptiert, lohnt sich die zusätzliche Arbeit, auf eine streamfähige Implementierung zu wechseln oder einen gepufferten Adapter zu bauen.

Wenn Sie in Services (Windows- oder Linux-Services) verschlüsseln, achten Sie außerdem auf sauberes Exception-Logging: „falsches Passwort“, „Header kaputt“, „Tag/HMAC ungültig“ sind unterschiedliche Betriebsfälle und sollten unterscheidbar sein. Wichtig dabei: Fehlermeldungen dürfen nach außen nicht zu detailliert sein (kein „Padding falsch in Block 7“ als API-Fehler), intern im Log aber schon.

Wann sich der Ansatz lohnt – und wo er kippen kann

Lohnt sich, wenn Sie: (a) verschlüsselte Export-/Importdaten langlebig speichern, (b) verschiedene Programmversionen parallel betreiben, (c) Daten als Streams verarbeiten oder (d) eine saubere Krypto-Schnittstelle für mehrere Module (Client/Server/Tooling) brauchen.

Kippt, wenn Sie versuchen, damit „alles“ zu lösen: Für Transport ist TLS zuständig, nicht ein selbstgebauter AES-Wrapper. Für Secrets (Passwörter, Tokens) ist oft ein OS-Secret-Store oder ein Vault sinnvoller. Und wenn Sie Interop mit anderen Sprachen benötigen, müssen Sie Header, Endianness und Encoding exakt dokumentieren (oder ein etabliertes Format nutzen).

Fazit: AES in Delphi ist weniger Algorithmus, mehr Engineering

Der eigentliche Gewinn dieses Schnipsels ist nicht „AES läuft“, sondern ein betriebsfähiges Format: zufälliger Salt und IV, versionierter Header, PBKDF2-Parameter im Payload und streamfähige Verarbeitung. Ergänzen Sie für neue Formate möglichst Integrität (AES-GCM oder Encrypt-then-HMAC). So wird aus „wir verschlüsseln irgendwas“ ein Baustein, der in digitalen Unternehmenslösungen auch nach Jahren noch wartbar und migrierbar ist.

Wenn Sie so einen Container in eine gewachsene Delphi-Landschaft integrieren oder von einem Legacy-Format sauber migrieren müssen, lohnt sich ein kurzer Architektur-Check (Schlüsselmanagement, Formatversionen, Betrieb/Logging). Details klären wir bei Bedarf gern im Gespräch:

Im fachlichen Umfeld spielen auch Delphi Aes und Pbkdf2 Delphi eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Bestand, Zielbild und technische Risiken werden zusammen bewertet.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spaetfolgen verschoben.
  • Sie sehen frueh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Beitrag teilen

Diesen Beitrag direkt weitergeben

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

E-Mail

Instagram oeffnet in einem neuen Tab. Link und Kurztext werden vorher in die Zwischenablage kopiert.