AES_ENCRYPT y AES_DECRYPT en MySQL
Las funciones AES_ENCRYPT() y AES_DECRYPT() implementan el estándar de cifrado AES (Advanced Encryption Standard) directamente en MySQL, permitiendo cifrar y descifrar datos sensibles a nivel de columna sin salir de la base de datos. A diferencia de las funciones de hash como MD5 o SHA2, que son unidireccionales y no permiten recuperar el dato original, el cifrado AES es bidireccional: puedes transformar un dato legible en una secuencia de bytes ilegible y luego revertir el proceso con la clave correcta. Esta capacidad es fundamental para proteger información personal, números de tarjetas de crédito, datos médicos y cualquier dato sujeto a regulaciones de privacidad como el RGPD. En esta guía cubriremos desde la sintaxis básica hasta la configuración avanzada con vectores de inicialización y modos de cifrado.
Diferencia entre hash y cifrado
Antes de profundizar en AES, es esencial comprender la diferencia conceptual entre funciones de hash y funciones de cifrado, ya que muchos desarrolladores las confunden y esto lleva a decisiones de diseño incorrectas.
Una función de hash como SHA2 transforma una entrada en un resumen de longitud fija de forma irreversible. No existe ninguna función que tome el hash y devuelva la cadena original. Esto las hace ideales para verificar integridad y almacenar tokens, pero inútiles cuando necesitas recuperar el dato original.
El cifrado AES, en cambio, transforma una entrada en una secuencia cifrada usando una clave. Con esa misma clave, puedes revertir la transformación y obtener el dato original. Es como una caja fuerte: guardas un documento dentro, la cierras con llave, y solo quien tenga esa llave puede abrirla y leer el documento.
-- Hash: unidireccional, no se puede revertir
SELECT SHA2('dato secreto', 256);
-- No existe función para volver a 'dato secreto'
-- Cifrado: bidireccional, se puede revertir con la clave
SELECT AES_ENCRYPT('dato secreto', 'mi_clave');
-- Se puede recuperar el original:
SELECT AES_DECRYPT(AES_ENCRYPT('dato secreto', 'mi_clave'), 'mi_clave');
-- Resultado: 'dato secreto'Sintaxis de AES_ENCRYPT y AES_DECRYPT
La sintaxis básica de ambas funciones requiere dos argumentos obligatorios: los datos a cifrar o descifrar y la clave de cifrado. Opcionalmente, se puede proporcionar un vector de inicialización (IV) como tercer argumento cuando se utiliza un modo de cifrado que lo requiera.
AES_ENCRYPT(datos, clave [, vector_inicializacion])
AES_DECRYPT(datos_cifrados, clave [, vector_inicializacion])AES_ENCRYPT devuelve un valor de tipo BINARY, es decir, una secuencia de bytes crudos que no se puede mostrar directamente como texto legible. AES_DECRYPT toma esa secuencia de bytes y la clave, y devuelve los datos originales. Si la clave proporcionada al descifrar no coincide con la usada al cifrar, el resultado será NULL.
-- Cifrar un dato
SELECT AES_ENCRYPT('información confidencial', 'clave_secreta_2026');
-- Resultado: secuencia de bytes (no legible)
-- Cifrar y ver el resultado en hexadecimal
SELECT HEX(AES_ENCRYPT('información confidencial', 'clave_secreta_2026'));
-- Resultado: cadena hexadecimal legible
-- Descifrar
SELECT AES_DECRYPT(
AES_ENCRYPT('información confidencial', 'clave_secreta_2026'),
'clave_secreta_2026'
);
-- Resultado: 'información confidencial'
-- Descifrar con clave incorrecta
SELECT AES_DECRYPT(
AES_ENCRYPT('información confidencial', 'clave_secreta_2026'),
'clave_incorrecta'
);
-- Resultado: NULLAlmacenar datos cifrados con HEX/UNHEX
Dado que AES_ENCRYPT devuelve datos binarios, necesitas una estrategia para almacenarlos correctamente en tu base de datos. Hay dos enfoques principales: usar una columna de tipo VARBINARY o BLOB para almacenar los bytes directamente, o convertir a hexadecimal con HEX() y almacenar en una columna VARCHAR.
El enfoque con VARBINARY es más eficiente en espacio porque almacena los bytes crudos. El enfoque con HEX()/UNHEX() ocupa el doble de espacio pero es más portable y fácil de depurar. Para datos de tipo BLOB, el almacenamiento binario directo suele ser la mejor opción.
-- Enfoque 1: columna VARBINARY (más eficiente)
CREATE TABLE datos_sensibles_v1 (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email_cifrado VARBINARY(512) NOT NULL,
telefono_cifrado VARBINARY(256)
);
INSERT INTO datos_sensibles_v1 (nombre, email_cifrado, telefono_cifrado)
VALUES (
'Carlos Ruiz',
AES_ENCRYPT('carlos.ruiz@empresa.com', 'clave_maestra'),
AES_ENCRYPT('+34 698 765 432', 'clave_maestra')
);
-- Recuperar datos descifrados
SELECT
nombre,
CAST(AES_DECRYPT(email_cifrado, 'clave_maestra') AS CHAR) AS email,
CAST(AES_DECRYPT(telefono_cifrado, 'clave_maestra') AS CHAR) AS telefono
FROM datos_sensibles_v1;Observa el uso de CAST(... AS CHAR) al descifrar. Esto es necesario porque AES_DECRYPT devuelve un valor binario y, sin el cast, MySQL podría mostrar los datos como una secuencia de bytes en lugar de texto legible.
-- Enfoque 2: columna VARCHAR con HEX/UNHEX (más portable)
CREATE TABLE datos_sensibles_v2 (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email_cifrado VARCHAR(1024) NOT NULL,
telefono_cifrado VARCHAR(512)
);
INSERT INTO datos_sensibles_v2 (nombre, email_cifrado, telefono_cifrado)
VALUES (
'Laura Martínez',
HEX(AES_ENCRYPT('laura.martinez@empresa.com', 'clave_maestra')),
HEX(AES_ENCRYPT('+34 611 222 333', 'clave_maestra'))
);
-- Recuperar datos descifrados
SELECT
nombre,
CAST(AES_DECRYPT(UNHEX(email_cifrado), 'clave_maestra') AS CHAR) AS email,
CAST(AES_DECRYPT(UNHEX(telefono_cifrado), 'clave_maestra') AS CHAR) AS telefono
FROM datos_sensibles_v2;Modos de cifrado: block_encryption_mode
MySQL permite configurar el modo de cifrado AES mediante la variable de sistema block_encryption_mode. Esta variable determina tanto el tamaño de la clave (128, 192 o 256 bits) como el modo de operación del cifrado por bloques (ECB o CBC).
El formato de la variable es aes-tamaño-modo. Por defecto, MySQL usa aes-128-ecb, que es el modo más simple pero también el menos seguro. El modo ECB (Electronic Codebook) cifra cada bloque de 16 bytes de forma independiente, lo que significa que bloques idénticos en el texto plano producirán bloques idénticos en el texto cifrado, revelando patrones en los datos.
-- Ver el modo actual
SELECT @@block_encryption_mode;
-- Resultado por defecto: aes-128-ecb
-- Cambiar a AES-256 con modo CBC (recomendado)
SET SESSION block_encryption_mode = 'aes-256-cbc';
-- Verificar el cambio
SELECT @@block_encryption_mode;
-- Resultado: aes-256-cbcEl modo CBC (Cipher Block Chaining) es significativamente más seguro que ECB porque cada bloque cifrado depende del anterior, eliminando los patrones repetitivos. Sin embargo, CBC requiere un vector de inicialización (IV) como tercer parámetro de las funciones de cifrado.
-- Modos disponibles:
-- aes-128-ecb (por defecto, sin IV)
-- aes-128-cbc (requiere IV de 16 bytes)
-- aes-192-ecb (sin IV)
-- aes-192-cbc (requiere IV de 16 bytes)
-- aes-256-ecb (sin IV)
-- aes-256-cbc (requiere IV, recomendado)Vector de inicialización (IV) con modo CBC
Cuando usas un modo CBC, necesitas proporcionar un vector de inicialización de exactamente 16 bytes. El IV debe ser único para cada operación de cifrado, aunque no necesita ser secreto. Su propósito es garantizar que cifrar el mismo dato con la misma clave produzca resultados diferentes cada vez, impidiendo que un atacante pueda deducir patrones.
La forma más segura de generar un IV en MySQL es usando RANDOM_BYTES, que produce bytes criptográficamente seguros.
-- Configurar AES-256-CBC
SET SESSION block_encryption_mode = 'aes-256-cbc';
-- Generar un IV aleatorio de 16 bytes
SET @iv = RANDOM_BYTES(16);
SET @clave = 'clave_de_cifrado_muy_segura_2026';
SET @datos = 'Número de tarjeta: 4532-XXXX-XXXX-7890';
-- Cifrar con IV
SELECT HEX(AES_ENCRYPT(@datos, @clave, @iv)) AS dato_cifrado;
-- Para descifrar, necesitas la misma clave Y el mismo IV
SELECT CAST(AES_DECRYPT(
AES_ENCRYPT(@datos, @clave, @iv),
@clave,
@iv
) AS CHAR) AS dato_descifrado;Es fundamental almacenar el IV junto con los datos cifrados, ya que sin él no podrás descifrar la información. El IV no necesita estar cifrado, así que puedes guardarlo en una columna separada en texto plano o en formato hexadecimal.
CREATE TABLE datos_pacientes (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre_paciente VARCHAR(100) NOT NULL,
diagnostico_cifrado VARBINARY(1024) NOT NULL,
medicacion_cifrada VARBINARY(512),
iv VARBINARY(16) NOT NULL,
fecha_consulta DATE NOT NULL
);
-- Insertar con IV único por registro
SET SESSION block_encryption_mode = 'aes-256-cbc';
INSERT INTO datos_pacientes (nombre_paciente, diagnostico_cifrado, medicacion_cifrada, iv, fecha_consulta)
VALUES (
'Ana García Pérez',
AES_ENCRYPT('Hipertensión arterial grado 2', @clave, @iv := RANDOM_BYTES(16)),
AES_ENCRYPT('Enalapril 10mg/día', @clave, @iv),
@iv,
'2026-03-24'
);Para recuperar los datos, usas el IV almacenado en cada registro.
SELECT
nombre_paciente,
CAST(AES_DECRYPT(diagnostico_cifrado, @clave, iv) AS CHAR) AS diagnostico,
CAST(AES_DECRYPT(medicacion_cifrada, @clave, iv) AS CHAR) AS medicacion,
fecha_consulta
FROM datos_pacientes
WHERE nombre_paciente = 'Ana García Pérez';Ejemplo completo: cifrar datos de contacto
Veamos un ejemplo completo que implementa cifrado AES-256-CBC en un escenario realista: una tabla de clientes donde los emails y teléfonos deben estar cifrados para cumplir con normativas de protección de datos.
-- Configuración inicial
SET SESSION block_encryption_mode = 'aes-256-cbc';
CREATE TABLE clientes_protegidos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email_cifrado VARBINARY(512) NOT NULL,
telefono_cifrado VARBINARY(256),
iv VARBINARY(16) NOT NULL,
hash_email CHAR(64) NOT NULL COMMENT 'Para búsquedas sin descifrar',
fecha_registro DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_hash_email (hash_email)
);
-- Procedimiento para insertar clientes con cifrado
DELIMITER //
CREATE PROCEDURE insertar_cliente(
IN p_nombre VARCHAR(100),
IN p_email VARCHAR(200),
IN p_telefono VARCHAR(20),
IN p_clave VARCHAR(64)
)
BEGIN
DECLARE v_iv VARBINARY(16);
SET v_iv = RANDOM_BYTES(16);
INSERT INTO clientes_protegidos
(nombre, email_cifrado, telefono_cifrado, iv, hash_email)
VALUES (
p_nombre,
AES_ENCRYPT(p_email, p_clave, v_iv),
AES_ENCRYPT(p_telefono, p_clave, v_iv),
v_iv,
SHA2(LOWER(p_email), 256)
);
END //
DELIMITER ;
-- Usar el procedimiento
CALL insertar_cliente(
'Roberto Sánchez',
'roberto.sanchez@ejemplo.com',
'+34 677 889 900',
'clave_maestra_empresa_2026_segura'
);Observa la columna hash_email con un hash SHA2 del email. Esto permite buscar clientes por email sin necesidad de descifrar todos los registros, lo que sería extremadamente lento en tablas grandes.
-- Buscar un cliente por email sin descifrar toda la tabla
SELECT
nombre,
CAST(AES_DECRYPT(email_cifrado, 'clave_maestra_empresa_2026_segura', iv) AS CHAR) AS email,
CAST(AES_DECRYPT(telefono_cifrado, 'clave_maestra_empresa_2026_segura', iv) AS CHAR) AS telefono
FROM clientes_protegidos
WHERE hash_email = SHA2(LOWER('roberto.sanchez@ejemplo.com'), 256);Gestión de claves de cifrado
La seguridad de todo el sistema de cifrado depende de la protección de la clave. Si la clave se compromete, todos los datos cifrados quedan expuestos. Existen varias estrategias para gestionar las claves de forma segura, y la elección depende de la infraestructura y el nivel de seguridad requerido.
La regla más importante es nunca incluir la clave directamente en las queries SQL almacenadas en código fuente. Un atacante que acceda al código tendría acceso inmediato a todos los datos cifrados.
-- ❌ NUNCA hagas esto en código de producción
SELECT AES_DECRYPT(email_cifrado, 'mi_clave_en_texto_plano', iv)
FROM clientes_protegidos;
-- ✅ Usar variables de sesión establecidas por la aplicación
-- La aplicación establece la clave al conectar:
SET @encryption_key = ?; -- Valor enviado como parámetro desde la app
-- Las queries usan la variable, nunca la clave literal
SELECT AES_DECRYPT(email_cifrado, @encryption_key, iv)
FROM clientes_protegidos;Otra opción es usar funciones definidas por el usuario o variables de sistema configuradas a nivel de servidor, accesibles solo por usuarios con privilegios específicos. En entornos empresariales, la clave se almacena en un servicio de gestión de secretos externo como HashiCorp Vault o AWS KMS, y la aplicación la recupera al iniciar la conexión.
Errores comunes con AES_ENCRYPT y AES_DECRYPT
El error más frecuente es obtener NULL al descifrar y no saber por qué. Esto ocurre cuando la clave de descifrado no coincide exactamente con la de cifrado, incluyendo diferencias en espacios, codificación o mayúsculas.
-- La clave debe ser exactamente la misma
SET @cifrado = AES_ENCRYPT('datos', 'Clave123');
SELECT AES_DECRYPT(@cifrado, 'Clave123'); -- Funciona
SELECT AES_DECRYPT(@cifrado, 'clave123'); -- NULL (mayúscula diferente)
SELECT AES_DECRYPT(@cifrado, 'Clave123 '); -- NULL (espacio extra)Otro error común es olvidar el CAST(... AS CHAR) al descifrar. Sin él, MySQL devuelve el resultado como un valor binario que puede mostrarse como caracteres ilegibles o causar problemas al concatenar con otras cadenas.
El tercer error frecuente es usar modo CBC sin proporcionar un IV, o proporcionar un IV de longitud incorrecta. El IV debe tener exactamente 16 bytes. Si tiene una longitud diferente, MySQL puede devolver NULL o producir resultados inesperados.
SET SESSION block_encryption_mode = 'aes-256-cbc';
-- ❌ Sin IV en modo CBC: resultado impredecible
SELECT AES_ENCRYPT('datos', 'clave');
-- ❌ IV de longitud incorrecta
SELECT AES_ENCRYPT('datos', 'clave', 'iv_corto');
-- ✅ IV correcto de 16 bytes
SELECT AES_ENCRYPT('datos', 'clave', RANDOM_BYTES(16));Cuándo usar AES_ENCRYPT y AES_DECRYPT
El cifrado a nivel de columna con AES es apropiado cuando necesitas proteger campos específicos que contienen datos sensibles: emails, teléfonos, números de documento, datos médicos o información financiera. Es especialmente relevante para cumplir con regulaciones como el RGPD, la HIPAA o el PCI-DSS, que exigen cifrado de datos personales en reposo.
Sin embargo, cifrar columnas tiene un coste: no puedes crear índices funcionales sobre datos cifrados, las búsquedas requieren descifrar o usar hashes auxiliares, y el rendimiento de las consultas se ve afectado. Por eso, cifra solo las columnas que realmente lo necesitan, no toda la tabla. Para los datos donde solo necesitas verificar integridad sin recuperar el original, las funciones de hash como SHA2 son más apropiadas y eficientes.
Escrito por Eduardo Lázaro
