Net-Base Revista

30.05.2026

Cifrado AES en Delphi: un fragmento de código fuente robusto con IV, salt, encabezado y streaming

Un fragmento de código fuente Delphi práctico para cifrado AES con sal e IV aleatorios, estructura clara de encabezado de archivo, derivación de clave PBKDF2 y streaming — incluyendo las trampas típicas en formatos legacy, integridad y operación.

30.05.2026

Del tema de la revista a la práctica del proyecto

Páginas de servicios y técnicas relacionadas

En Cifrado AES Delphi en la práctica rara vez falla «AES en sí», sino las condiciones circundantes: los datos deben procesarse como flujo (archivos, BLOBs, copias de seguridad), los formatos antiguos deben seguir siendo legibles, y en explotación se necesita capacidad de depuración (encabezado, versionado) y valores predeterminados seguros (Salt/IV aleatorios, sin reutilización). Este fragmento de código fuente muestra por tanto no solo «Encrypt/Decrypt», sino un pequeño formato robusto con encabezado, versión, Salt e IV — además de PBKDF2 para la derivación de la clave y un punto donde tiene sentido añadir integridad.

Por qué «cifrar una cadena con AES» casi nunca es suficiente

En el software empresarial a medida, el cifrado aparece típicamente en tres lugares: (1) configuración/secrets (p. ej., credenciales), (2) archivos de intercambio/exportación y (3) datos en reposo (p. ej., archivos, contenedores de documentos). El enfoque ingenuo «contraseña → clave AES → cadena entra/sale» se desequilibra rápidamente:

  • Reutilización del IV: en modos como CBC o GCM un vector de inicialización (IV) debe ser único por cifrado. Un IV constante es una fuga, incluso si la contraseña es fuerte.
  • Clave desde la contraseña sin KDF: usar una contraseña directamente como clave (o hashearla una vez) facilita ataques offline. Una KDF (función de derivación de claves) como PBKDF2 ralentiza a los atacantes de forma deliberada.
  • Sin versión de formato: sin encabezado/versionado será difícil cambiar más adelante el número de iteraciones, el algoritmo o los parámetros sin dejar datos antiguos «huérfanos».
  • Sin integridad: AES-CBC cifra, pero no impide la manipulación. Sin autenticación (p. ej., HMAC o AEAD como GCM) se expone a ataques de bitflipping/problemas de padding y a fallos difíciles de diagnosticar.

El núcleo de este artículo: un pequeño formato contenedor que admite streaming, es versionable y evita los errores habituales.

Cifrado AES Delphi con encabezado, Salt, IV y PBKDF2

Definimos un formato contenedor simple, que también puede usarse en BLOBs de base de datos o en payloads de mensajes:

  • Magic: 4 bytes, p. ej. NBAE (comprobación rápida «¿es este nuestro formato?»)
  • Versión: 1 byte (permite migración)
  • Parámetros KDF: número de iteraciones (4 bytes)
  • Salt: 16 bytes (aleatorio por archivo)
  • IV: 16 bytes (aleatorio por archivo para AES-CBC)
  • Ciphertext: datos útiles cifrados (apto para streaming)

Importante: Salt e IV no son secretos. Solo deben ser nuevos por cada cifrado. La contraseña permanece secreta; la clave derivada no se almacena.

Cifrado AES Delphi en streaming: escribir/leer el contenedor

El código está deliberadamente escrito como un «plan»: funciones claramente separadas, encabezados comprobables, sin variables globales ocultas. Para AES y PBKDF2 muchos equipos usan una biblioteca criptográfica consolidada (p. ej., DEC). El fragmento muestra el formato y el patrón de streaming; las llamadas a AES/PBKDF2 están encapsuladas de modo que puede sustituirlas según la biblioteca.

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;

// — Dependencias que debe implementar según la pila criptográfica —

procedure FillRandomBytes(var B: TBytes);
begin
// Para entropía criptográfica: use el CSPRNG del SO (Windows BCryptGenRandom,
// Linux getrandom/urandom). Aquí, deliberadamente, como marcador de posición.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG no enlazado‘);
end;

function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementación p. ej. con DEC (PBKDF2) u otra biblioteca.
// Resultado: AKeyLen bytes.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: no enlazado‘);
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementación mediante biblioteca:
// – KeyLen = 32 bytes
// – IVLen = 16 bytes
// – PKCS#7 Padding
// Importante: procesar orientado a stream, no todo en memoria.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: no enlazado‘);
end;

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

// — Funciones auxiliares —

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚No se pudo escribir el encabezado‘);
end;

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

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(‚Contenedor no válido (Magic no coincide)‘);

if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Versión de contenedor desconocida: %d‘, [H.Version]);

if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Número de iteraciones fuera de límites plausibles‘);

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(‚La contraseña no puede estar vacía‘);

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

// Rellenar encabezado
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);

// Derivar clave (32 bytes para AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);

// Cifrar los datos (el ciphertext sigue inmediatamente tras el encabezado)
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(‚La contraseña no puede estar vacía‘);

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

// Descifrar desde la posición actual del stream (tras el encabezado)
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.

Propósito: Un contenedor mínimo adecuado para archivos y BLOBs, incluyendo versionado y parámetros KDF. Restricciones: Debe conectar con un CSPRNG real (aleatoriedad criptográficamente segura del sistema operativo) y disponer de una implementación robusta de AES/PBKDF2 debajo. Peligros: No use cualquier generador aleatorio (no Random()), no emplee IVs fijos y planifique un manejo de errores claro al desencriptar (contraseña incorrecta vs. datos dañados). Variantes: en lugar de CBC prefiera AEAD (ver más abajo), o amplíe el encabezado con ID de algoritmo y HMAC.

Integridad: por qué AES-CBC por sí solo es arriesgado en producción

AES-CBC sigue presente en muchos entornos legacy y puede funcionar si utiliza además una protección de integridad. Sin integridad, un atacante puede manipular el ciphertext; incluso sin un atacante activo, errores de transmisión o capas de almacenamiento defectuosas producen errores de „padding“ difíciles de diagnosticar.

Opciones pragmáticas:

  • Encrypt-then-HMAC: Escriba un HMAC (p. ej. HMAC-SHA-256) sobre Header+Ciphertext tras el ciphertext. Al leer, verifique primero el HMAC y luego desencripte. Para ello derive idealmente dos claves desde PBKDF2 (p. ej. 64 bytes: 32 para AES, 32 para HMAC), en lugar de reutilizar la misma clave en doble función.
  • AES-GCM: Modo AEAD (Authenticated Encryption with Associated Data). Produce ciphertext + Auth-Tag. Hoy en día suele ser la elección más clara, si su biblioteca Delphi soporta GCM de forma estable. Los campos del encabezado pueden autenticarse como „AAD“ sin necesidad de cifrarlos.

Si debe quedarse con CBC (p. ej. por interoperabilidad), Encrypt-then-HMAC es la adición robusta. Para formatos nuevos merece la pena GCM, porque incorpora autenticación y clarifica los perfiles de error.

Inusualmente importante: «aleatoriedad criptográfica» y por qué System.Hash no basta

Un reflejo legacy común en proyectos Delphi: «hacemos SHA256 de la marca temporal + algo y ya tenemos random». Eso no es una base fiable. Para sal y IV necesita un CSPRNG (Generador de números pseudoaleatorios criptográficamente seguro) del sistema operativo. Bajo Windows suele ser la BCrypt-API (CNG), bajo Linux un generador del kernel como getrandom() o /dev/urandom. La diferencia práctica es: un CSPRNG está diseñado para que, a partir de valores observados, no sea posible predecir valores futuros.

Truco de arquitectura: encapsúlelo en una pequeña unidad „RandomProvider“ que pueda mockearse en pruebas. Así resuelve dos casos: pruebas reproducibles (con seed fijo en el mock) y seguridad real en producción (con CSPRNG del SO). También evita que en un hotfix „por rapidez“ vuelva a introducirse Random().

Depuración y migración legacy: el versionado no es un lujo

El encabezado no es solo «belleza criptográfica», sino mantenibilidad:

  • Ajuste de iteraciones: los valores de iteración de PBKDF2 cambian con los años. Con un campo en el encabezado puede aumentarlos más tarde sin dejar datos antiguos inservibles.
  • Cambio de formato: la versión 2 podría, por ejemplo, pasar a AES-GCM o añadir un HMAC.
  • Diagnóstico en campo: Magic/Version permiten comprobaciones rápidas en logs y herramientas sin necesidad de desencriptar los datos.

Consejo práctico: Implemente un pequeño «inspector» que solo lea el encabezado (Magic/Version/Iterations) y lo registre en un log. Con eso aclarará muchos casos de soporte («¿Qué versión hay aquí?») sin gestionar contraseñas.

Migración limpia: „Read old, write new“ en lugar de Big Bang

Si va a sustituir un formato antiguo (p. ej. IV fijo, sin KDF, Blowfish/3DES o XOR casero), en proyectos Delphi se ha mostrado eficaz este patrón: al leer detecte varios formatos (Magic/Version o heurística de fallback); al escribir produzca solo el formato nuevo. Adicionalmente, tras un descifrado exitoso puede re-encriptar en segundo plano («lazy migration») si encaja en su proceso. Así reduce el riesgo del despliegue y evita un mantenimiento tipo «re-encriptar todo de una vez».

Hilos y streaming: puntos críticos típicos en Delphi

El cifrado suele ejecutarse en worker-threads (p. ej. en exportaciones, al subir a un portal de cliente, o al escribir archivos de gran tamaño). Dos aspectos que aparecen con regularidad en proyectos Delphi:

  • Posición del stream: Antes de cifrar/descifrar establezca contratos claros: el stream de entrada se lee desde la posición actual, el stream de salida se escribe desde la posición actual. Si reutiliza streams asegúrese conscientemente de hacer Position := 0.
  • Picos de memoria: Evite «todo en TBytes». El enfoque por streams es esencial para ficheros grandes. Si su biblioteca crypto solo acepta arrays de bytes, merece la pena el trabajo adicional para pasar a una implementación apta para streams o construir un adaptador con buffering.

Si cifra dentro de servicios (Windows- o Linux-Services), pRESTe además atención a un logging de excepciones limpio: «contraseña incorrecta», «encabezado dañado», «Tag/HMAC inválido» son casos operativos diferentes y deben poder distinguirse. Importante: los mensajes de error no deben ser demasiado detallados hacia el exterior (no mostrar «Padding incorrecto en bloque 7» como error de API), pero en los logs internos sí pueden ser descriptivos.

Cuándo vale la pena el enfoque — y dónde puede fallar

Vale la pena si usted: (a) almacena datos de exportación/importación cifrados a largo plazo, (b) opera versiones de programa distintas en paralelo, (c) procesa datos como streams o (d) necesita una interfaz cripto limpia para varios módulos (Client/Server/Tooling).

Falla si intenta resolver con esto «todo»: para el transporte está TLS, no un wrapper AES hecho por usted. Para los secrets (contraseñas, tokens) suele ser más apropiado un almacén de secretos del sistema operativo o un Vault. Y si necesita interoperabilidad con otros lenguajes, debe documentar con precisión el header, el endianness y el encoding (o usar un formato ya establecido).

Conclusión: AES en Delphi es menos algoritmo, más ingeniería

La ganancia real de este fragmento no es «AES funciona», sino un formato operativo: salt e IV aleatorios, header versionado, parámetros PBKDF2 en el payload y procesamiento apto para streams. Añada integridad en los formatos nuevos (AES-GCM o Encrypt-then-HMAC). Así, de «ciframos algo» obtiene un componente que en soluciones empresariales digitales seguirá siendo mantenible y migrable incluso pasados años.

Si necesita integrar un contenedor así en un entorno Delphi consolidado o migrarlo de forma limpia desde un formato heredado, conviene realizar una breve revisión de arquitectura (gestión de claves, versiones de formato, operación/registro). Aclaramos los detalles en una conversación si es necesario:

En el ámbito funcional, Delphi Aes y Pbkdf2 Delphi también desempeñan un papel importante cuando las integraciones, los flujos de datos y la evolución deben interactuar de forma coherente.

Discutir un proyecto o una iniciativa de modernización con Net-Base.

Siguiente paso

Cuando el tema se convierte en un proyecto real, la arquitectura, los sistemas existentes y la operación deben considerarse desde el principio.

No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.

  • La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
  • REST, el acceso a datos, los portales y el rollout no se posponen como consecuencias tardías.
  • Detecta con antelación qué enfoque es viable desde el punto de vista económico y operativo.

Compartir entrada

Compartir esta publicación directamente

LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y un texto breve de inmediato.

Correo electrónico

Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.