
🔒 Cifrado y descifrado de archivos en PHP: OpenSSL y Sodium en lugar de mcrypt
Cifrar un archivo antes de guardarlo en el servidor y tener la certeza de que nadie, ni siquiera el administrador del hosting, puede leerlo sin la clave. Parece una necesidad básica, pero con la salida de mcrypt de PHP, el enfoque conocido dejó de funcionar.
La extensión mcrypt fue marcada como obsoleta en PHP 7.1 y completamente eliminada en PHP 7.2 en 2017. Hoy, en PHP 8, intentar llamar a mcrypt_encrypt() termina en un error fatal. Mientras tanto, los archivos confidenciales siguen en los servidores: copias de seguridad de bases de datos, CSVs con datos personales, contratos PDF, exportaciones de pedidos.
Buenas noticias: PHP ofrece dos mecanismos funcionales de serie, OpenSSL y Sodium. Ambos no requieren instalación de extensiones adicionales en un hosting moderno, ambos son más rápidos y seguros que mcrypt. A continuación, una guía práctica para cifrar y descifrar archivos en PHP 8 con código que puede copiar y ejecutar.
💡 Resumen rápido:
- Por qué mcrypt está muerto y qué versiones de PHP están afectadas
- Cifrado de archivos paso a paso mediante OpenSSL con AES-256-CBC
- Descifrado con protección contra manipulación de datos mediante HMAC
- Alternativa Sodium para PHP 7.2 y superior
- Cuándo elegir OpenSSL y cuándo Sodium
Por qué mcrypt ya no es una opción
La biblioteca mcrypt no se ha actualizado desde 2007. Se encontraron vulnerabilidades críticas en su código y no quedaban mantenedores. El equipo de PHP tomó una decisión: en PHP 7.1 la extensión se marcó como obsoleta, y en PHP 7.2, lanzado en noviembre de 2017, fue completamente excluida del núcleo.
Si está migrando un proyecto antiguo con mcrypt, revise phpinfo(). En PHP 7.2+ no existe la línea "mcrypt support: enabled". Llamar a mcrypt_encrypt(), mcrypt_decrypt() o a los filtros de flujo mcrypt.tripledes / mdecrypt.tripledes devuelve el error "Call to undefined function".
Técnicamente, mcrypt está disponible a través de PECL con el comando pecl install mcrypt. Pero instalar una extensión sin soporte y con vulnerabilidades conocidas en un servidor de producción para un script heredado es una mala idea. Reescriba el cifrado con OpenSSL: está integrado en PHP desde la versión 5.3 y no va a desaparecer.
Cifrado de archivos mediante OpenSSL, paso a paso
OpenSSL en PHP está representado por las funciones openssl_encrypt() y openssl_decrypt(). Trabajan con datos sin procesar y admiten docenas de algoritmos, desde AES-128-CBC hasta AES-256-GCM. Para archivos usamos AES-256-CBC: es criptográficamente fuerte y no requiere PHP 7.1, a diferencia de GCM con parámetros de etiqueta adicionales.
Paso 1: generar la clave de cifrado
La clave es el secreto principal de todo el esquema. Debe ser criptográficamente aleatoria, no inventada manualmente. Nada de "contraseña-secreta" de los ejemplos, solo openssl_random_pseudo_bytes().
El siguiente script genera una clave de 256 bits y la muestra en formato para insertar en wp-config.php. Ejecútelo una vez por línea de comandos y guarde el resultado:
1 <?php 2 // Generating a random 256-bit key (32 bytes) 3 $encryption_key = base64_encode(openssl_random_pseudo_bytes(32)); 4 echo "define('FILE_ENCRYPTION_KEY', '" . $encryption_key . "');\n";
La función openssl_random_pseudo_bytes(32) devuelve 32 bytes de aleatoriedad de calidad criptográfica. base64_encode convierte los datos binarios en una cadena fácil de almacenar en archivos de configuración. La clave debe estar fuera del document root, en wp-config.php o .env, pero no en el código del tema.
Paso 2: función de cifrado de archivos
El script lee un archivo del disco, lo cifra con el algoritmo AES-256-CBC, añade un IV aleatorio al inicio y una firma HMAC para verificar la integridad, y guarda el resultado. Añada el código a functions.php de un tema hijo o a un plugin personalizado.
Advertencia: antes de ejecutarlo en un servidor de producción, haga una copia de seguridad completa. Pruebe el cifrado-descifrado en una copia del archivo en un directorio de prueba. Si se pierde la clave, descifrar los datos es imposible, AES-256 no se puede descifrar por fuerza bruta.
1 <?php 2 function encrypt_file(string $sourcePath, string $destPath, string $key): bool 3 { 4 if (!file_exists($sourcePath)) { 5 throw new RuntimeException('Source file not found: ' . $sourcePath); 6 } 7 8 $plaintext = file_get_contents($sourcePath); 9 if ($plaintext === false) { 10 throw new RuntimeException('Failed to read file'); 11 } 12 13 $cipher = 'aes-256-cbc'; 14 $ivLength = openssl_cipher_iv_length($cipher); 15 $iv = openssl_random_pseudo_bytes($ivLength); 16 17 $ciphertext = openssl_encrypt( 18 $plaintext, 19 $cipher, 20 base64_decode($key), 21 OPENSSL_RAW_DATA, 22 $iv 23 ); 24 25 if ($ciphertext === false) { 26 throw new RuntimeException('Encryption error'); 27 } 28 29 // HMAC signature for integrity verification during decryption 30 $hmac = hash_hmac('sha256', $iv . $ciphertext, base64_decode($key), true); 31 32 // File format: IV (16 bytes) + HMAC (32 bytes) + ciphertext 33 $result = file_put_contents($destPath, $iv . $hmac . $ciphertext); 34 35 return $result !== false; 36 }
Lo que sucede aquí línea por línea:
openssl_cipher_iv_length('aes-256-cbc')devuelve 16, la longitud del vector de inicialización para este algoritmo.openssl_random_pseudo_bytes($ivLength)crea un IV aleatorio. Hace que datos idénticos cifrados con la misma clave produzcan un texto cifrado diferente en cada ejecución.OPENSSL_RAW_DATAle indica a la función que devuelva datos binarios, no base64. Guardamos el texto cifrado sin procesar por compacidad.hash_hmac('sha256', ...)calcula una suma de verificación del conjunto IV-y-texto-cifrado. Durante el descifrado recalcularemos el HMAC y lo compararemos: si los datos fueron modificados o se corrompieron, la comparación no coincidirá.- El archivo se guarda en formato:
[IV 16 bytes][HMAC 32 bytes][ciphertext]. Sin delimitadores, las posiciones están fijadas por las longitudes.
Paso 3: función de descifrado
El proceso inverso: leer IV, leer HMAC, leer texto cifrado, recalcular HMAC y comparar mediante hash_equals(), descifrar. El código se añade al mismo archivo:
1 <?php 2 function decrypt_file(string $sourcePath, string $key): string|false 3 { 4 if (!file_exists($sourcePath)) { 5 throw new RuntimeException('Encrypted file not found: ' . $sourcePath); 6 } 7 8 $data = file_get_contents($sourcePath); 9 if ($data === false) { 10 throw new RuntimeException('Failed to read file'); 11 } 12 13 $cipher = 'aes-256-cbc'; 14 $ivLength = openssl_cipher_iv_length($cipher); 15 $hmacLength = 32; // sha256 = 32 bytes 16 17 $iv = substr($data, 0, $ivLength); 18 $hmac = substr($data, $ivLength, $hmacLength); 19 $ciphertext = substr($data, $ivLength + $hmacLength); 20 21 // Integrity check: recompute HMAC and compare 22 $calculatedHmac = hash_hmac( 23 'sha256', 24 $iv . $ciphertext, 25 base64_decode($key), 26 true 27 ); 28 29 if (!hash_equals($hmac, $calculatedHmac)) { 30 throw new RuntimeException('File corrupted or key invalid'); 31 } 32 33 $plaintext = openssl_decrypt( 34 $ciphertext, 35 $cipher, 36 base64_decode($key), 37 OPENSSL_RAW_DATA, 38 $iv 39 ); 40 41 return $plaintext; 42 }
Punto clave: hash_equals() en lugar de ===. La comparación de cadenas normal es vulnerable a ataques de temporización, un atacante puede deducir el HMAC byte a byte midiendo el tiempo de respuesta del servidor. hash_equals() compara cadenas en tiempo constante sin importar en qué carácter difieran.
Ejemplo de uso con rutas reales
1 <?php 2 $key = FILE_ENCRYPTION_KEY; // from wp-config.php 3 4 // Encrypting the database backup 5 encrypt_file( 6 __DIR__ . '/backup.sql', 7 __DIR__ . '/backup.sql.enc', 8 $key 9 ); 10 11 // Decrypting and serving for download 12 $decrypted = decrypt_file(__DIR__ . '/backup.sql.enc', $key); 13 header('Content-Type: application/octet-stream'); 14 header('Content-Disposition: attachment; filename="backup.sql"'); 15 echo $decrypted;
Las funciones son universales: funcionan con cualquier tipo de archivo, imágenes, PDF, CSV, volcados SQL. El tamaño está limitado solo por la RAM disponible, ya que el archivo se lee completamente en memoria. Para archivos de gigabytes se necesitará procesamiento por flujo con búfer por fragmentos, pero para la inmensa mayoría de las tareas prácticas este código es suficiente.
Alternativa: Sodium (libsodium)
La extensión Sodium está integrada en PHP a partir de la versión 7.2 y pasó a formar parte del núcleo en PHP 8.1. Proporciona una API más simple en comparación con OpenSSL: no es necesario gestionar manualmente el IV y el HMAC, el cifrado autenticado funciona de serie.
1 <?php 2 function sodium_encrypt_file(string $sourcePath, string $destPath, string $key): bool 3 { 4 $plaintext = file_get_contents($sourcePath); 5 $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 6 $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, base64_decode($key)); 7 return file_put_contents($destPath, $nonce . $ciphertext) !== false; 8 } 9 10 function sodium_decrypt_file(string $sourcePath, string $key): string|false 11 { 12 $data = file_get_contents($sourcePath); 13 $nonce = substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 14 $ciphertext = substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 15 $result = sodium_crypto_secretbox_open($ciphertext, $nonce, base64_decode($key)); 16 return $result !== false ? $result : false; 17 }
Nota: Sodium verifica la integridad por sí mismo. Si los datos fueron modificados o la clave es incorrecta, sodium_crypto_secretbox_open() simplemente devuelve false, sin necesidad de comprobaciones HMAC separadas. El código tiene la mitad de extensión.
Una desventaja: Sodium requiere PHP 7.2 o superior. Si el proyecto corre en PHP 7.0-7.1, la única opción es OpenSSL. Pero en la práctica, en 2026, encontrar un hosting con PHP inferior a 7.4 ya es difícil: según los datos de WordPress.org de junio de 2026, la cuota de PHP 7.0-7.1 es inferior al 0.3% de todas las instalaciones.
OpenSSL o Sodium: dos criterios de selección
La elección entre los dos enfoques se reduce a dos factores:
- Versión de PHP. PHP 7.2+, elija Sodium, es más seguro por defecto y no le permite estropear la implementación del HMAC. PHP 7.0-7.1, solo OpenSSL. Por debajo de PHP 7.0, es hora de actualizar el servidor, no de inventar soluciones alternativas con PECL-mcrypt.
- Portabilidad de datos entre entornos. OpenSSL está disponible en todas partes, incluido PHP 5.3+. Si los archivos cifrados deben ser legibles en el servidor de desarrollo, en producción y en el del cliente, OpenSSL es más fiable desde el punto de vista de la compatibilidad. El texto cifrado de Sodium solo se descifrará donde exista Sodium.
En la práctica, usamos OpenSSL en proyectos donde la compatibilidad entre diferentes entornos es importante. Sodium, donde toda la pila está actualizada a PHP 8 y la seguridad es lo primero.
En el vídeo, Dave Hollingworth desglosa ambos enfoques en detalle con demostración de código y explicación de los primitivos criptográficos detrás de cada uno. El material complementa el artículo: se muestran casos límite, la instalación de la biblioteca defuse/php-encryption mediante Composer y la comparación de rendimiento de OpenSSL y Sodium con datos reales.
⁉️🤔 Preguntas frecuentes
¿Puedo usar md5() o sha1() para el cifrado de archivos?
No.
md5()ysha1()son funciones hash, son irreversibles por definición. Un archivo cifrado debe poder descifrarse, y un hash no se puede descifrar. Los hashes se utilizan para la verificación de integridad (como el HMAC en el código anterior) y el almacenamiento de contraseñas mediantepassword_hash(), pero no para el cifrado de contenido.
¿Qué hacer si se pierde la clave de cifrado?
Descifrar los datos sin la clave es imposible. Almacene la clave en
wp-config.phpfuera del document root y haga una copia de seguridad de la misma por separado de las copias de archivos y bases de datos. No ponga la clave en un repositorio Git, añadawp-config.phpa.gitignoreo use variables de entorno.
¿Por qué se necesita el IV si la clave ya es secreta?
Sin un IV aleatorio, datos idénticos cifrados con la misma clave producen un texto cifrado idéntico. Un atacante que vea bloques repetidos obtiene información sobre la estructura del archivo. El IV hace que cada ejecución de cifrado sea única: el mismo archivo cifrado dos veces con una misma clave produce dos textos cifrados diferentes.
¿Puedo cifrar archivos grandes, de varios gigabytes?
Las funciones anteriores leen el archivo completamente en memoria, para datos de gigabytes esto provocará un agotamiento de la memoria. Para el cifrado por flujo, use
openssl_encrypt()en un bucle con búfer por fragmentos (por ejemplo, de 1 MB cada uno) o la bibliotecadefuse/php-encryption, que admite el modo de flujo de serie.
¿Funciona este código en PHP 8.3?
Sí. Tanto OpenSSL como Sodium son totalmente compatibles con PHP 8. El código está probado en versiones actuales de PHP y no utiliza funciones obsoletas. En PHP 8.3 se mantiene la compatibilidad, no hay incompatibilidades retroactivas en la extensión OpenSSL.
Cifrado de archivos en 2026: veredicto práctico
Mcrypt abandonó PHP, y es para bien. Las dos herramientas de reemplazo integradas son más seguras y rápidas, y no requieren bailes con pandereta a través de PECL. OpenSSL funciona en todas partes, Sodium es más simple y fiable por defecto.
En resumen: proyecto nuevo en PHP 8, empiece con Sodium, el código resulta más limpio. Migrando código antiguo desde mcrypt, reescriba a OpenSSL, está disponible incluso en PHP 7.0. Y la regla principal de la criptografía: almacene las claves por separado de los datos cifrados. La pérdida de la clave equivale a la pérdida de datos, y la fuerza bruta no ayudará aquí.
Comience con un script de prueba en una copia del archivo: asegúrese de que el ciclo "cifrado → descifrado → coincidencia byte a byte" funciona sin errores. ¿Y qué método usa usted en sus proyectos, OpenSSL, Sodium u otro? Escriba en los comentarios.



