Cifrar datos en PHP va más allá de usar md5() o base64_encode(): esas funciones no son cifrado. El cifrado real transforma los datos de modo que solo quien tiene la clave pueda recuperarlos. PHP ofrece dos caminos modernos para hacerlo: la extensión openssl con algoritmos estándar y libsodium, disponible desde PHP 7.2, que es más sencilla de usar correctamente.
openssl_encrypt con AES-256-GCM
AES-256-GCM es el modo recomendado para cifrado simétrico: proporciona confidencialidad y autenticidad del mensaje (AEAD), lo que protege tanto el contenido como su integridad:
<?php
function cifrar(string $texto, string $clave): string {
$metodo = 'aes-256-gcm';
$iv = random_bytes(openssl_cipher_iv_length($metodo)); // 12 bytes para GCM
$tag = '';
$cifrado = openssl_encrypt(
$texto,
$metodo,
$clave, // debe tener exactamente 32 bytes para AES-256
OPENSSL_RAW_DATA,
$iv,
$tag, // authentication tag (16 bytes)
'', // datos adicionales autenticados (AAD) — opcional
16 // longitud del tag en bytes
);
if ($cifrado === false) {
throw new RuntimeException('Error al cifrar: ' . openssl_error_string());
}
// Concatenar IV + tag + cifrado, y codificar en base64 para almacenamiento
return base64_encode($iv . $tag . $cifrado);
}
function descifrar(string $datos_b64, string $clave): string {
$metodo = 'aes-256-gcm';
$datos = base64_decode($datos_b64, true);
$iv_len = openssl_cipher_iv_length($metodo);
$iv = substr($datos, 0, $iv_len);
$tag = substr($datos, $iv_len, 16);
$cifrado = substr($datos, $iv_len + 16);
$texto = openssl_decrypt(
$cifrado,
$metodo,
$clave,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($texto === false) {
throw new RuntimeException('Descifrado fallido: datos corruptos o clave incorrecta');
}
return $texto;
}
// Uso
$clave = random_bytes(32); // 32 bytes para AES-256 — guarda esto de forma segura
$texto = 'Número de tarjeta: 4111-1111-1111-1111';
$cifrado = cifrar($texto, $clave);
$original = descifrar($cifrado, $clave);
echo $original; // Número de tarjeta: 4111-1111-1111-1111
Gestionar la clave de cifrado
La clave es lo más importante. Nunca la almacenes en el mismo lugar que los datos cifrados:
<?php
// Opción 1: variable de entorno (la más sencilla)
$clave = base64_decode($_ENV['ENCRYPTION_KEY']);
// En .env: ENCRYPTION_KEY=base64_de_32_bytes_aleatorios
// Generar: echo base64_encode(random_bytes(32));
// Opción 2: derivar desde una contraseña maestra con PBKDF2
function derivarClave(string $password, string $salt): string {
return hash_pbkdf2('sha256', $password, $salt, 100000, 32, true);
}
$salt = random_bytes(32);
$clave = derivarClave('password_maestra', $salt);
// Guarda el salt junto a los datos; la contraseña maestra en un gestor de secretos
// Opción 3: fichero de clave fuera del webroot
$clave = file_get_contents('/etc/miapp/encryption.key');
libsodium: cifrado más sencillo y seguro
libsodium está incluida en PHP desde 7.2 sin necesidad de instalar nada. Sus funciones son más difÃciles de usar mal que las de openssl:
<?php
// sodium_crypto_secretbox: cifrado simétrico autenticado
$clave = sodium_crypto_secretbox_keygen(); // 32 bytes aleatorios
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24 bytes
$mensaje = 'Datos confidenciales del paciente';
$cifrado = sodium_crypto_secretbox($mensaje, $nonce, $clave);
$almacenar = base64_encode($nonce . $cifrado);
// Descifrar
$datos = base64_decode($almacenar, true);
$nonce2 = substr($datos, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cifr2 = substr($datos, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$original = sodium_crypto_secretbox_open($cifr2, $nonce2, $clave);
if ($original === false) {
throw new RuntimeException('Descifrado fallido');
}
echo $original; // Datos confidenciales del paciente
// IMPORTANTE: limpiar la clave de la memoria
sodium_memzero($clave);
secretstream: cifrado de ficheros grandes
Para ficheros grandes, secretstream cifra por fragmentos sin cargar todo en memoria:
<?php
function cifrarFichero(string $entrada, string $salida, string $clave): void {
$in = fopen($entrada, 'rb');
$out = fopen($salida, 'wb');
[$estado, $cabecera] = sodium_crypto_secretstream_xchacha20poly1305_init_push($clave);
fwrite($out, $cabecera);
while (!feof($in)) {
$fragmento = fread($in, 65536); // 64 KB por fragmento
$etiqueta = feof($in)
? SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL
: SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
fwrite($out, sodium_crypto_secretstream_xchacha20poly1305_push($estado, $fragmento, '', $etiqueta));
}
fclose($in); fclose($out);
}
function descifrarFichero(string $entrada, string $salida, string $clave): void {
$in = fopen($entrada, 'rb');
$out = fopen($salida, 'wb');
$cabecera = fread($in, SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES);
$estado = sodium_crypto_secretstream_xchacha20poly1305_init_pull($cabecera, $clave);
while (!feof($in)) {
$fragmento_cifrado = fread($in, 65536 + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES);
[$fragmento] = sodium_crypto_secretstream_xchacha20poly1305_pull($estado, $fragmento_cifrado);
fwrite($out, $fragmento);
}
fclose($in); fclose($out);
}
$clave = sodium_crypto_secretstream_xchacha20poly1305_keygen();
cifrarFichero('backup.sql', 'backup.sql.enc', $clave);
descifrarFichero('backup.sql.enc', 'backup_restaurado.sql', $clave);
El antipatrón AES-CBC sin autenticación
AES-CBC sin un MAC (Message Authentication Code) es vulnerable a ataques de padding oracle. Es el error histórico más común con openssl en PHP:
<?php
// MAL — AES-CBC sin autenticación del mensaje
function cifrarMal(string $texto, string $clave): string {
$iv = random_bytes(16);
$cifrado = openssl_encrypt($texto, 'aes-256-cbc', $clave, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $cifrado);
// Sin tag de autenticación ? vulnerable a ataques de manipulación
}
// BIEN — usa siempre GCM o CCM (modos AEAD) que autentican el mensaje
// O usa libsodium que hace esto por defecto
Cifrado asimétrico con openssl
Para compartir datos entre partes sin compartir la clave:
<?php
// Generar par de claves (normalmente se hace una vez y se guarda en ficheros)
$config = [
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
$recurso = openssl_pkey_new($config);
openssl_pkey_export($recurso, $clavePrivada);
$clavePublica = openssl_pkey_get_details($recurso)['key'];
// Cifrar con la clave pública (solo el dueño de la privada puede descifrar)
openssl_public_encrypt('Datos secretos', $cifrado, $clavePublica);
$almacenar = base64_encode($cifrado);
// Descifrar con la clave privada
openssl_private_decrypt(base64_decode($almacenar), $descifrado, $clavePrivada);
echo $descifrado; // Datos secretos
Buenas prácticas de cifrado en PHP
- Usa siempre modos AEAD: AES-256-GCM con openssl o secretbox/secretstream de libsodium. Nunca AES-CBC o AES-ECB sin MAC.
- IV/nonce aleatorio por mensaje: nunca reutilices el mismo IV con la misma clave. Un IV repetido con AES-GCM rompe la seguridad completamente.
- Limpia secretos de la memoria: usa
sodium_memzero()para borrar claves y datos sensibles de variables PHP. - No inventar esquemas propios: usa las primitivas de libsodium o los modos AEAD de openssl tal como están diseñados. La criptografÃa propia tiene errores no obvios.
- Las contraseñas se hashean, no se cifran: para contraseñas usa
password_hash()/password_verify(), no cifrado simétrico.
