RANDOM_BYTES en MySQL
La función RANDOM_BYTES() genera una secuencia de bytes aleatorios criptográficamente seguros en MySQL. A diferencia de RAND(), que utiliza un generador pseudoaleatorio predecible, RANDOM_BYTES() obtiene su entropía del generador de números aleatorios del sistema operativo, lo que garantiza que los valores producidos son impredecibles y aptos para uso en contextos de seguridad. Esta función es la pieza fundamental para generar sales de hashing, tokens de sesión, claves de API, vectores de inicialización para cifrado AES y cualquier valor donde la imprevisibilidad sea un requisito. Disponible desde MySQL 5.6.17, se ha convertido en una herramienta imprescindible para cualquier aplicación que maneje datos sensibles directamente desde la base de datos.
Sintaxis de RANDOM_BYTES
La función acepta un único parámetro entero que especifica la cantidad de bytes aleatorios a generar. El valor devuelto es de tipo BINARY, una secuencia de bytes crudos que no es directamente legible como texto.
RANDOM_BYTES(longitud)El parámetro longitud debe ser un valor entero entre 1 y 1024 inclusive. Si se proporciona un valor fuera de este rango, MySQL genera un error. La elección de la longitud depende del caso de uso: 16 bytes (128 bits) ofrecen seguridad suficiente para la mayoría de aplicaciones, mientras que 32 bytes (256 bits) proporcionan un margen de seguridad amplio incluso ante futuros avances en computación.
-- Generar 16 bytes aleatorios (128 bits)
SELECT RANDOM_BYTES(16);
-- Resultado: secuencia binaria de 16 bytes (no legible directamente)
-- Ver el resultado en formato hexadecimal legible
SELECT HEX(RANDOM_BYTES(16));
-- Resultado: cadena de 32 caracteres hexadecimales, ej: 'A7F3B2C801D9E5F4...'
-- Generar 32 bytes aleatorios (256 bits)
SELECT HEX(RANDOM_BYTES(32));
-- Resultado: cadena de 64 caracteres hexadecimales
-- Valores límite
SELECT HEX(RANDOM_BYTES(1)); -- 2 caracteres hex (1 byte)
SELECT HEX(RANDOM_BYTES(1024)); -- 2048 caracteres hex (1024 bytes, máximo)Cada invocación de RANDOM_BYTES() produce un valor completamente diferente, incluso con el mismo parámetro. Esta es la propiedad fundamental que la distingue de funciones deterministas como las de hash.
-- Tres llamadas consecutivas, tres resultados distintos
SELECT HEX(RANDOM_BYTES(8)) AS r1,
HEX(RANDOM_BYTES(8)) AS r2,
HEX(RANDOM_BYTES(8)) AS r3;
-- Los tres valores serán completamente diferentesRepresentación legible con HEX
Dado que RANDOM_BYTES() devuelve datos binarios, la función HEX() es tu aliada constante para convertir esos bytes en una representación hexadecimal legible. Cada byte se convierte en dos caracteres hexadecimales, por lo que la salida de HEX(RANDOM_BYTES(n)) siempre tiene exactamente 2*n caracteres.
Esta conversión es útil tanto para depuración como para almacenamiento. Si necesitas guardar el valor en una columna VARCHAR en lugar de VARBINARY, la representación hexadecimal es la opción más directa. Para operaciones posteriores que requieran el valor binario original, la función UNHEX() revierte la conversión.
-- Generar, almacenar como hex, y recuperar como binario
SET @bytes_originales = RANDOM_BYTES(16);
SET @hex_string = HEX(@bytes_originales);
-- La conversión es reversible
SELECT @bytes_originales = UNHEX(@hex_string) AS son_iguales;
-- Resultado: 1 (verdadero)También puedes usar la codificación Base64 con TO_BASE64() si prefieres una representación más compacta. Base64 produce cadenas más cortas que hexadecimal porque utiliza 64 caracteres distintos en lugar de 16.
-- Comparación de formatos para 32 bytes aleatorios
SET @datos = RANDOM_BYTES(32);
SELECT
LENGTH(HEX(@datos)) AS longitud_hex, -- 64 caracteres
LENGTH(TO_BASE64(@datos)) AS longitud_base64; -- ~44 caracteresGenerar sales para hashes
Una sal es un valor aleatorio que se antepone o concatena a los datos antes de calcular un hash. Su propósito es garantizar que dos entradas idénticas produzcan hashes diferentes, frustrando ataques basados en tablas rainbow o diccionarios precalculados. RANDOM_BYTES() es la forma correcta de generar sales en MySQL porque produce valores impredecibles y no repetibles.
La combinación de RANDOM_BYTES() con SHA2 y CONCAT forma un patrón robusto para generar hashes con sal.
CREATE TABLE tokens_usuario (
id INT AUTO_INCREMENT PRIMARY KEY,
usuario_id INT NOT NULL,
sal VARBINARY(32) NOT NULL,
token_hash CHAR(64) NOT NULL,
proposito ENUM('verificacion_email', 'recuperar_password', 'api_key') NOT NULL,
creado_en DATETIME DEFAULT CURRENT_TIMESTAMP,
expira_en DATETIME NOT NULL,
usado BOOLEAN DEFAULT FALSE,
INDEX idx_token (token_hash),
INDEX idx_usuario (usuario_id)
);Al generar un token, primero creas una sal aleatoria, luego un valor aleatorio como base del token, y finalmente combinas ambos con SHA2 para producir el hash que almacenas. El token original (en formato hexadecimal) es lo que envías al usuario, y el hash es lo que guardas en la base de datos.
-- Generar un token de verificación de email
SET @sal = RANDOM_BYTES(32);
SET @token_crudo = RANDOM_BYTES(32);
SET @token_para_usuario = HEX(@token_crudo);
SET @token_hash = SHA2(CONCAT(HEX(@sal), ':', HEX(@token_crudo)), 256);
INSERT INTO tokens_usuario (usuario_id, sal, token_hash, proposito, expira_en)
VALUES (
157,
@sal,
@token_hash,
'verificacion_email',
DATE_ADD(NOW(), INTERVAL 48 HOUR)
);
-- El valor @token_para_usuario se envía al usuario por email
-- Cuando el usuario lo presenta, se verifica así:
SELECT id, usuario_id
FROM tokens_usuario
WHERE token_hash = SHA2(CONCAT(HEX(sal), ':', @token_para_usuario), 256)
AND expira_en > NOW()
AND usado = FALSE;Este patrón asegura que incluso si un atacante accede a la base de datos, solo encontrará hashes, no los tokens originales. Y gracias a la sal única por registro, cada hash es diferente aunque dos usuarios hubieran recibido por coincidencia el mismo token.
Tokens de sesión y claves de API
Los tokens de sesión y las claves de API son identificadores que otorgan acceso a recursos protegidos. Su seguridad depende directamente de la calidad de la aleatoriedad con la que se generan. Un token predecible permite a un atacante suplantrar sesiones legítimas.
CREATE TABLE sesiones (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
usuario_id INT NOT NULL,
token_sesion CHAR(64) NOT NULL UNIQUE,
ip_origen VARCHAR(45),
user_agent VARCHAR(255),
creado_en DATETIME DEFAULT CURRENT_TIMESTAMP,
ultimo_acceso DATETIME DEFAULT CURRENT_TIMESTAMP,
expira_en DATETIME NOT NULL,
INDEX idx_token (token_sesion),
INDEX idx_usuario (usuario_id)
);
-- Crear nueva sesión con token seguro
INSERT INTO sesiones (usuario_id, token_sesion, ip_origen, user_agent, expira_en)
VALUES (
42,
HEX(RANDOM_BYTES(32)),
'192.168.1.100',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
DATE_ADD(NOW(), INTERVAL 8 HOUR)
);Para claves de API de larga duración, es habitual generar tokens más largos y añadir un prefijo que identifique el tipo de clave. Esta convención facilita la rotación de claves y la identificación visual del tipo de credencial.
CREATE TABLE api_keys (
id INT AUTO_INCREMENT PRIMARY KEY,
usuario_id INT NOT NULL,
nombre_clave VARCHAR(100) NOT NULL,
prefijo CHAR(8) NOT NULL,
clave_hash CHAR(64) NOT NULL UNIQUE,
permisos JSON,
creado_en DATETIME DEFAULT CURRENT_TIMESTAMP,
ultimo_uso DATETIME,
activa BOOLEAN DEFAULT TRUE,
INDEX idx_prefijo (prefijo),
INDEX idx_hash (clave_hash)
);
-- Generar API key con prefijo identificable
SET @clave_cruda = HEX(RANDOM_BYTES(32));
SET @prefijo = LEFT(@clave_cruda, 8);
SET @clave_completa = CONCAT('tmysql_', @clave_cruda);
SET @clave_hash = SHA2(@clave_completa, 256);
INSERT INTO api_keys (usuario_id, nombre_clave, prefijo, clave_hash, permisos)
VALUES (
42,
'Clave producción dashboard',
@prefijo,
@clave_hash,
'{"lectura": true, "escritura": false, "admin": false}'
);
-- La @clave_completa se muestra al usuario UNA sola vez
-- Para verificar: buscar por prefijo y comparar hash
SELECT id, nombre_clave, permisos
FROM api_keys
WHERE prefijo = LEFT('ABC12345...', 8)
AND clave_hash = SHA2('tmysql_ABC12345...resto_de_la_clave', 256)
AND activa = TRUE;Vector de inicialización para AES en modo CBC
Uno de los usos técnicos más importantes de RANDOM_BYTES() es la generación de vectores de inicialización (IV) para el cifrado AES_ENCRYPT en modo CBC. El IV debe tener exactamente 16 bytes y ser único para cada operación de cifrado, aunque no necesita ser secreto.
Usar un IV aleatorio garantiza que cifrar el mismo dato con la misma clave produzca resultados diferentes cada vez, lo que impide que un atacante pueda deducir patrones en los datos cifrados.
SET SESSION block_encryption_mode = 'aes-256-cbc';
-- Generar IV con RANDOM_BYTES
SET @clave = 'clave_cifrado_empresa_2026_segura';
SET @iv = RANDOM_BYTES(16);
-- Cifrar datos sensibles
SET @email_cifrado = AES_ENCRYPT('contacto@empresa.com', @clave, @iv);
-- El mismo dato cifrado dos veces con IVs diferentes
-- produce resultados completamente distintos
SET @iv1 = RANDOM_BYTES(16);
SET @iv2 = RANDOM_BYTES(16);
SELECT
HEX(AES_ENCRYPT('mismo dato', @clave, @iv1)) AS cifrado_1,
HEX(AES_ENCRYPT('mismo dato', @clave, @iv2)) AS cifrado_2;
-- Los resultados son diferentes gracias a los IVs distintosRecuerda que el IV debe almacenarse junto con los datos cifrados, ya que es necesario para descifrar. Puedes guardarlo en una columna dedicada de tipo VARBINARY(16) en la misma fila que los datos cifrados.
RANDOM_BYTES frente a RAND
Es común ver desarrolladores que usan RAND() para generar tokens o valores de seguridad. Esta práctica es insegura y debe evitarse. Aunque RAND() produce valores que parecen aleatorios, utiliza un generador pseudoaleatorio determinista cuya secuencia puede predecirse si se conoce la semilla.
RANDOM_BYTES(), en cambio, utiliza el generador de números aleatorios del sistema operativo, que se alimenta de fuentes de entropía física como la temporización de interrupciones del hardware, movimientos del ratón y ruido eléctrico.
-- ❌ RAND() no es criptográficamente seguro
-- Un atacante podría predecir la secuencia
SELECT MD5(CONCAT(RAND(), NOW())) AS token_inseguro;
-- ✅ RANDOM_BYTES() es criptográficamente seguro
-- Imposible predecir el siguiente valor
SELECT HEX(RANDOM_BYTES(32)) AS token_seguro;La diferencia es especialmente crítica en entornos donde la seguridad importa. Si usas RAND() con FLOOR(RAND() * ...) para generar códigos de verificación de dos factores, un atacante que conozca la implementación de MySQL podría predecir los códigos futuros. Con RANDOM_BYTES(), cada código es genuinamente impredecible.
-- Generar código de verificación 2FA de 6 dígitos
-- ❌ Con RAND (predecible)
SELECT LPAD(FLOOR(RAND() * 1000000), 6, '0') AS codigo_inseguro;
-- ✅ Con RANDOM_BYTES (seguro)
-- Convertir bytes aleatorios a número y tomar módulo
SELECT LPAD(
CONV(LEFT(HEX(RANDOM_BYTES(4)), 8), 16, 10) % 1000000,
6,
'0'
) AS codigo_seguro;Ejemplos prácticos adicionales
Veamos algunos patrones prácticos que combinan RANDOM_BYTES() con otras funciones para resolver problemas reales de seguridad en aplicaciones.
El primer ejemplo genera identificadores de transacción únicos y seguros para un sistema de pagos. Estos identificadores necesitan ser únicos globalmente e imposibles de adivinar.
CREATE TABLE transacciones_pago (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
referencia CHAR(48) NOT NULL UNIQUE,
comprador_id INT NOT NULL,
importe DECIMAL(12,2) NOT NULL,
divisa CHAR(3) DEFAULT 'EUR',
estado ENUM('pendiente', 'completada', 'fallida', 'reembolsada') DEFAULT 'pendiente',
creado_en DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
INDEX idx_referencia (referencia)
);
-- Generar referencia única tipo 'PAY-XXXXXXXX-XXXX-XXXX'
INSERT INTO transacciones_pago (referencia, comprador_id, importe, divisa)
VALUES (
CONCAT('PAY-',
LEFT(HEX(RANDOM_BYTES(8)), 8), '-',
LEFT(HEX(RANDOM_BYTES(4)), 4), '-',
LEFT(HEX(RANDOM_BYTES(4)), 4)
),
1042,
299.99,
'EUR'
);El segundo ejemplo implementa un sistema de enlaces de descarga temporales donde cada enlace contiene un token seguro con fecha de expiración.
CREATE TABLE enlaces_descarga (
id INT AUTO_INCREMENT PRIMARY KEY,
archivo_id INT NOT NULL,
token CHAR(64) NOT NULL UNIQUE,
descargas_permitidas SMALLINT DEFAULT 1,
descargas_realizadas SMALLINT DEFAULT 0,
expira_en DATETIME NOT NULL,
creado_por INT NOT NULL,
INDEX idx_token (token)
);
-- Crear enlace de descarga temporal
INSERT INTO enlaces_descarga (archivo_id, token, descargas_permitidas, expira_en, creado_por)
VALUES (
587,
HEX(RANDOM_BYTES(32)),
3,
DATE_ADD(NOW(), INTERVAL 72 HOUR),
42
);
-- Validar token al intentar descargar
UPDATE enlaces_descarga
SET descargas_realizadas = descargas_realizadas + 1
WHERE token = 'el_token_recibido'
AND expira_en > NOW()
AND descargas_realizadas < descargas_permitidas;Errores comunes con RANDOM_BYTES
El error más habitual es intentar generar más de 1024 bytes en una sola llamada. MySQL lanza un error si el parámetro excede este límite. Si necesitas más bytes, puedes hacer múltiples llamadas y concatenar los resultados con CONCAT.
-- ❌ Error: longitud fuera de rango
SELECT RANDOM_BYTES(2048);
-- ERROR 1690: RANDOM_BYTES length out of range
-- ✅ Si necesitas más bytes, concatenar
SELECT CONCAT(RANDOM_BYTES(1024), RANDOM_BYTES(1024)) AS bytes_2048;Otro error frecuente es almacenar el resultado binario en una columna VARCHAR sin convertirlo previamente a hexadecimal. Los datos binarios pueden contener bytes que no corresponden a caracteres válidos, causando corrupción silenciosa o truncamiento.
-- ❌ Almacenar binario en VARCHAR: posible corrupción
INSERT INTO tabla (token) VALUES (RANDOM_BYTES(32));
-- ✅ Convertir a hex antes de almacenar en VARCHAR
INSERT INTO tabla (token) VALUES (HEX(RANDOM_BYTES(32)));
-- ✅ O usar una columna VARBINARY
ALTER TABLE tabla MODIFY token VARBINARY(32);
INSERT INTO tabla (token) VALUES (RANDOM_BYTES(32));Cuándo usar RANDOM_BYTES
Utiliza RANDOM_BYTES() siempre que necesites valores aleatorios para cualquier propósito relacionado con la seguridad. Esto incluye la generación de sales para hashes, tokens de sesión, claves de API, vectores de inicialización para AES_ENCRYPT, códigos de verificación de dos factores, referencias únicas de transacciones y enlaces temporales con token.
No necesitas RANDOM_BYTES() para tareas donde la seguridad no es relevante, como seleccionar registros aleatorios de una tabla (ORDER BY RAND()) o generar datos de prueba. En esos casos, RAND() es perfectamente adecuado y más sencillo de usar. La regla general es simple: si un atacante podría beneficiarse de predecir el valor, usa RANDOM_BYTES(); si no, RAND() es suficiente.
Escrito por Eduardo Lázaro
