From magazine topic to project implementation
Relevant service and technical pages for this post
In AES Verschlüsselung Delphi practical failures rarely stem from „AES itself“ but from boundary conditions: data must be processed as a stream (files, BLOBs, backups), legacy formats must remain readable, and in operation you need debuggability (header, versioning) and secure defaults (salt/IV random, no reuse). This source snippet therefore shows not only „Encrypt/Decrypt“ but a small, robust format with header, version, salt and IV – plus PBKDF2 for key derivation and a place where integrity can be sensibly added.
Why „AES string encryption“ is almost never sufficient
In bespoke enterprise software encryption typically appears in three places: (1) configuration/secrets (e.g. credentials), (2) exchange/export files and (3) at-rest data (e.g. archives, document containers). The naive approach „password → AES key → string in/out“ fails quickly:
- IV reuse: For modes like CBC or GCM an initialization vector (IV) must be unique per encryption. A constant IV is a leak, even if the password is strong.
- Key from password without KDF: Using a password directly as a key (or hashing it once) invites offline attacks. A KDF (Key Derivation Function) like PBKDF2 deliberately slows attackers.
- No format version: Without a header/version you can hardly change iteration counts, algorithm or parameters later without orphaning old data.
- No integrity: AES-CBC provides confidentiality but not protection against tampering. Without authentication (e.g. HMAC or an AEAD mode like GCM) you get bit-flipping/padding issues and hard-to-diagnose failures.
The core of this article: a small container format that supports streaming, is versionable and avoids common mistakes.
AES Verschlüsselung Delphi with header, salt, IV and PBKDF2
We define a simple container format that can also be used in database BLOBs or message payloads:
- Magic: 4 bytes, e.g.
NBAE(quick „Is this our format?“ check) - Version: 1 byte (allows migration)
- KDF parameters: iteration count (4 bytes)
- Salt: 16 bytes (random per file)
- IV: 16 bytes (random per file for AES-CBC)
- Ciphertext: encrypted payload (streaming-capable)
Important: salt and IV are not secret. They only need to be new for each encryption. The password remains secret; the derived key is not stored.
AES Verschlüsselung Delphi in the stream: writing/reading the container
The code is intentionally written as a „blueprint“: clearly separated functions, verifiable headers, no hidden globals. For AES and PBKDF2 many teams use a proven crypto library (e.g. DEC). The snippet shows the format and the streaming pattern; the AES/PBKDF2 calls are encapsulated so you can swap them depending on your chosen library.
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;
// — Dependencies you must implement depending on the crypto stack —
procedure FillRandomBytes(var B: TBytes);
begin
// For cryptographic randomness: use the OS CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Intentionally left as a placeholder here.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG not bound‘);
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementation e.g. via DEC (PBKDF2) or another library.
// Result: AKeyLen bytes.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: not bound‘);
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementation using a library:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Important: process in a stream-oriented manner, do not load everything into memory.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: not bound‘);
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: not bound‘);
end;
// — Helper —
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header could not be written‘);
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header incomplete‘);
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(‚Not a valid container (magic does not match)‘);
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Unknown container version: %d‘, [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Iteration count outside plausible bounds‘);
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(‚Password must not be empty‘);
// Generate Salt/IV
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// Populate header
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);
// Derive key (32 bytes for AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// Encrypt payload (ciphertext follows directly after 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(‚Password must not be empty‘);
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);
// Decrypt starting at the current stream position (after the 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.
Purpose: A minimal container suitable for files and BLOBs, including versioning and KDF parameters. Constraints: You must layer a real CSPRNG binding (cryptographically secure randomness from the operating system) and a robust AES/PBKDF2 implementation underneath. Pitfalls: Don’t use “any” random (no Random()), no fixed IVs, and plan for distinct error handling on decrypt (wrong password vs. corrupted data). Variants: prefer AEAD instead of CBC (see below), or extend the header with algorithm ID and HMAC.
Integrity: why AES-CBC alone is too risky in production
AES-CBC is still present in many legacy contexts and can work if you additionally use integrity protection. Without integrity an attacker can manipulate the ciphertext; even without an active attacker, transmission errors or faulty storage layers produce hard-to-diagnose „padding“ errors.
Pragmatic options:
- Encrypt-then-HMAC: Append an HMAC (e.g. HMAC-SHA-256) over Header+Ciphertext after the ciphertext. On read, verify the HMAC first, then decrypt. For that you should ideally derive two keys from PBKDF2 (e.g. 64 bytes: 32 for AES, 32 for HMAC) instead of reusing the same key twice.
- AES-GCM: AEAD mode (Authenticated Encryption with Associated Data). Produces ciphertext + auth tag. This is often the cleanest choice today if your Delphi library supports GCM stably. Header fields can be authenticated as „AAD“ without needing to encrypt them.
If you must stick with CBC (e.g. for interoperability), Encrypt-then-HMAC is the robust complement. For new formats GCM is worthwhile because it gives you authentication „for free“ and makes error patterns clearer.
Unusually important: „cryptographic randomness“ and why System.Hash is not enough
A common legacy reflex in Delphi projects: „We just take SHA256 over a timestamp + something and call it random.“ That is not a reliable basis. For salt and IV you need an OS CSPRNG (Cryptographically Secure Pseudo Random Number Generator). Under Windows this is typically the BCrypt API (CNG), under Linux a kernel generator like getrandom() or /dev/urandom. The practical difference is: a CSPRNG is designed so that future values cannot be predicted from observed values.
Architectural trick: encapsulate this in a small „RandomProvider“ unit that you can mock in tests. That solves two edge cases at once: reproducible tests (with a fixed seed in the mock) and real security in production (with the OS CSPRNG). It prevents a hotfix from „just“ reintroducing Random() because it’s quicker.
Debugging and legacy migration: versioning is not a luxury
The header is not just for „crypto neatness“, but for maintainability:
- Iteration tuning: PBKDF2 iteration counts change over the years. With a header field you can raise them later without making old data unreadable.
- Format changes: Version 2 could, for example, switch to AES-GCM or add an HMAC.
- Field diagnostics: magic/version allow quick checks in logs and tools without decrypting data.
Practical tip: Implement a small „inspector“ that reads only the header (Magic/Version/Iterations) and writes it to a log. This resolves many support cases („Which version is this?“) without password handling.
Migrate cleanly: „Read old, write new“ instead of Big Bang
When deprecating an old format (e.g. fixed IV, no KDF, Blowfish/3DES, or a home‑grown XOR), a pattern has proven effective in Delphi projects: When reading, recognize multiple formats (Magic/Version or a fallback heuristic); when writing, produce only the new format. Additionally, upon successful decryption you can re-encrypt in the background („lazy migration“) if that fits the process. This reduces rollout risk and avoids „re-encrypt everything at once“ as a maintenance window.
Threading and Streaming: typical edge cases in Delphi
Encryption often runs in worker threads (e.g. during export, when uploading to a customer portal, or when writing large archives). Two points that regularly appear in Delphi projects:
- Stream positions: Define clear contracts before encrypting/decrypting: the input stream is read from its current position, the output stream is written from its current position. When reusing streams, be sure to explicitly set
Position := 0. - Memory spikes: Avoid „everything in terabytes.“ The stream approach is especially important for large files. If your crypto library only accepts byte arrays, it’s worth the extra work to switch to a stream-capable implementation or to build a buffered adapter.
When encrypting within services ( Windows- or Linux-services ), also pay attention to clean exception logging: „wrong password“, „header corrupted“, „tag/HMAC invalid“ are different operational cases and should be distinguishable. Important: error messages must not be too detailed externally (no „padding incorrect in block 7“ as an API error), but may be detailed internally in the logs.
When the approach pays off — and where it can break
Worthwhile when you: (a) store encrypted export/import data for the long term, (b) operate different application versions in parallel, (c) process data as streams, or (d) need a clean crypto interface for multiple modules (client/server/tooling).
Breaks when you try to solve „everything“ with it: TLS is responsible for transport, not a self-made AES wrapper. For secrets (passwords, tokens) an OS secret store or a vault is often more appropriate. And if you need interoperability with other languages, you must document header, endianness and encoding precisely (or use an established format).
Conclusion: AES in Delphi is less algorithm, more engineering
The real gain from this snippet is not „AES works“, but an operationally viable format: random salt and IV, a versioned header, PBKDF2 parameters in the payload, and stream-capable processing. For new formats add integrity where possible (AES-GCM or encrypt-then-HMAC). This turns „we encrypt something“ into a building block that remains maintainable and migratable in digital enterprise solutions even after years.
If you need to integrate such a container into an established Delphi landscape or migrate cleanly from a legacy format, a brief architecture check (key management, format versions, operation/logging) is worthwhile. We are happy to clarify details in a conversation if required:
In the domain context, Delphi Aes and Pbkdf2 Delphi also play an important role when integrations, data flows and further development must interoperate cleanly.
Discuss a project or modernization initiative with Net-Base.
Next step
When the topic becomes a real project, architecture, the existing system landscape and operations should be considered together early on.
We support not only with individual issues, but also when source snippets, legacy topics, or portal ideas are to be turned into a robust enterprise project.
- Current state, target state and technical risks are assessed jointly.
- REST, data access, portals and rollout are not deferred as afterthoughts.
- You can determine early which path is economically and operationally viable.