
🔒 Шифрование и дешифрование файлов в PHP: OpenSSL и Sodium вместо mcrypt
Зашифровать файл перед сохранением на сервер, и быть уверенным, что без ключа его не прочитает никто, даже администратор хостинга. Звучит как базовая потребность, но с уходом mcrypt из PHP привычный способ перестал работать.
Расширение mcrypt помечено deprecated в PHP 7.1 и полностью удалено в PHP 7.2 ещё в 2017 году. Сегодня, на PHP 8, попытка вызвать mcrypt_encrypt() заканчивается fatal error. Между тем на серверах по-прежнему лежат конфиденциальные файлы: бэкапы баз данных, CSV с персональными данными, PDF-договоры, экспорты заказов.
Хорошая новость: PHP из коробки даёт два работающих механизма, OpenSSL и Sodium. Оба не требуют установки дополнительных расширений на современном хостинге, оба быстрее и безопаснее mcrypt. Ниже, практическое руководство по шифрованию и расшифровке файлов на PHP 8 с кодом, который можно скопировать и запустить.
💡 Быстрый обзор:
- Почему mcrypt мёртв и какие версии PHP затронуты
- Пошаговое шифрование файла через OpenSSL с AES-256-CBC
- Расшифровка с защитой от подмены данных через HMAC
- Альтернатива на Sodium для PHP 7.2 и выше
- Когда выбирать OpenSSL, а когда Sodium
Почему mcrypt больше не вариант
Библиотека mcrypt не обновлялась с 2007 года. В её коде нашли критические уязвимости, а мейнтейнеров не осталось. Команда PHP приняла решение: в PHP 7.1 расширение помечено как устаревшее, а в PHP 7.2, вышедшем в ноябре 2017, полностью исключено из ядра.
Если вы переносите старый проект с mcrypt, загляните в phpinfo(). На PHP 7.2+ строчки «mcrypt support: enabled» там нет. Вызов mcrypt_encrypt(), mcrypt_decrypt() или stream-фильтров mcrypt.tripledes / mdecrypt.tripledes возвращает ошибку «Call to undefined function».
Формально mcrypt доступен через PECL, командой pecl install mcrypt. Но ставить неподдерживаемое расширение с известными уязвимостями на боевой сервер ради одного легаси-скрипта, плохая идея. Перепишите шифрование на OpenSSL: он встроен в PHP с версии 5.3 и никуда не денется.
Шифрование файла через OpenSSL: пошагово
OpenSSL в PHP представлен функциями openssl_encrypt() и openssl_decrypt(). Они работают с сырыми данными и поддерживают десятки алгоритмов, от AES-128-CBC до AES-256-GCM. Для файлов мы используем AES-256-CBC: он криптостойкий и не требует PHP 7.1 в отличие от GCM с дополнительными параметрами тегов.
Шаг 1: генерируем ключ шифрования
Ключ, главный секрет всей схемы. Он должен быть криптографически случайным, а не придуманным вручную. Никаких «secret-password» из примера, только openssl_random_pseudo_bytes().
Скрипт ниже генерирует 256-битный ключ и выводит его в формате для вставки в wp-config.php. Запустите один раз через командную строку и сохраните результат:
1 <?php 2 // Генерация случайного 256-битного ключа (32 байта) 3 $encryption_key = base64_encode(openssl_random_pseudo_bytes(32)); 4 echo "define('FILE_ENCRYPTION_KEY', '" . $encryption_key . "');\n";
Функция openssl_random_pseudo_bytes(32) возвращает 32 байта криптографически качественной случайности. base64_encode приводит бинарные данные к строке, которую удобно хранить в конфигурационных файлах. Ключ должен быть вне document root, в wp-config.php или .env, но не в коде темы.
Шаг 2: функция шифрования файла
Скрипт читает файл с диска, шифрует алгоритмом AES-256-CBC, добавляет в начало случайный IV и HMAC-подпись для проверки целостности, сохраняет результат. Добавьте код в functions.php дочерней темы или в кастомный плагин.
Внимание: перед запуском на боевом сервере сделайте полный бэкап. Протестируйте шифрование-расшифровку на копии файла в тестовой директории. Если ключ утерян, расшифровать данные невозможно, AES-256 не поддаётся брутфорсу.
1 <?php 2 function encrypt_file(string $sourcePath, string $destPath, string $key): bool 3 { 4 if (!file_exists($sourcePath)) { 5 throw new RuntimeException('Исходный файл не найден: ' . $sourcePath); 6 } 7 8 $plaintext = file_get_contents($sourcePath); 9 if ($plaintext === false) { 10 throw new RuntimeException('Не удалось прочитать файл'); 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('Ошибка шифрования'); 27 } 28 29 // HMAC-подпись для проверки целостности при расшифровке 30 $hmac = hash_hmac('sha256', $iv . $ciphertext, base64_decode($key), true); 31 32 // Формат файла: IV (16 байт) + HMAC (32 байта) + шифротекст 33 $result = file_put_contents($destPath, $iv . $hmac . $ciphertext); 34 35 return $result !== false; 36 }
Что здесь происходит построчно:
openssl_cipher_iv_length('aes-256-cbc')возвращает 16, длину вектора инициализации для этого алгоритма.openssl_random_pseudo_bytes($ivLength)создаёт случайный IV. Он делает так, что одинаковые данные, зашифрованные одним ключом, дают разный шифротекст при каждом запуске.OPENSSL_RAW_DATAуказывает функции вернуть бинарные данные, а не base64. Мы сохраняем сырой шифротекст для компактности.hash_hmac('sha256', ...)вычисляет контрольную сумму от связки IV-и-шифротекст. При расшифровке пересчитаем HMAC и сравним: если данные были изменены или повреждены, сравнение не сойдётся.- Файл сохраняется в формате:
[IV 16 байт][HMAC 32 байта][шифротекст]. Никаких разделителей, позиции фиксированы длинами.
Шаг 3: функция расшифровки
Обратный процесс: читаем IV, читаем HMAC, читаем шифротекст, пересчитываем HMAC и сравниваем через hash_equals(), расшифровываем. Код добавляется в тот же файл:
1 <?php 2 function decrypt_file(string $sourcePath, string $key): string|false 3 { 4 if (!file_exists($sourcePath)) { 5 throw new RuntimeException('Зашифрованный файл не найден: ' . $sourcePath); 6 } 7 8 $data = file_get_contents($sourcePath); 9 if ($data === false) { 10 throw new RuntimeException('Не удалось прочитать файл'); 11 } 12 13 $cipher = 'aes-256-cbc'; 14 $ivLength = openssl_cipher_iv_length($cipher); 15 $hmacLength = 32; // sha256 = 32 байта 16 17 $iv = substr($data, 0, $ivLength); 18 $hmac = substr($data, $ivLength, $hmacLength); 19 $ciphertext = substr($data, $ivLength + $hmacLength); 20 21 // Проверка целостности: пересчитываем HMAC и сравниваем 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('Файл повреждён или ключ неверен'); 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 }
Ключевой момент: hash_equals() вместо ===. Обычное сравнение строк подвержено timing-атакам, злоумышленник может подбирать HMAC побайтово, замеряя время ответа сервера. hash_equals() сравнивает строки за константное время независимо от того, на каком символе они разошлись.
Пример использования с реальными путями
1 <?php 2 $key = FILE_ENCRYPTION_KEY; // из wp-config.php 3 4 // Шифруем бэкап базы данных 5 encrypt_file( 6 __DIR__ . '/backup.sql', 7 __DIR__ . '/backup.sql.enc', 8 $key 9 ); 10 11 // Расшифровываем и отдаём на скачивание 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;
Функции универсальны: работают с любым типом файлов, изображения, PDF, CSV, SQL-дампы. Размер ограничен только доступной оперативной памятью, поскольку файл читается в память целиком. Для гигабайтных файлов потребуется потоковая обработка с буферизацией чанков, но для подавляющего большинства практических задач этого кода достаточно.
Альтернатива: Sodium (libsodium)
Расширение Sodium встроено в PHP начиная с версии 7.2 и стало частью ядра в PHP 8.1. Оно предоставляет более простой API по сравнению с OpenSSL: не нужно вручную управлять IV и HMAC, аутентифицированное шифрование работает из коробки.
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 }
Обратите внимание: Sodium сам проверяет целостность. Если данные были изменены или ключ неверен, sodium_crypto_secretbox_open() просто возвращает false, никаких отдельных HMAC-проверок не требуется. Код получается вдвое короче.
Недостаток один: Sodium требует PHP 7.2 или выше. Если проект крутится на PHP 7.0-7.1, единственный вариант, OpenSSL. Но на практике в 2026 году найти хостинг с PHP ниже 7.4 уже сложно: по данным WordPress.org на июнь 2026, доля PHP 7.0-7.1 составляет менее 0,3% от всех установок.
OpenSSL или Sodium: два критерия выбора
Выбор между двумя подходами сводится к двум факторам:
- Версия PHP. PHP 7.2+, берите Sodium, он безопаснее по умолчанию и не даёт ошибиться с реализацией HMAC. PHP 7.0-7.1, только OpenSSL. Ниже PHP 7.0, пора обновлять сервер, а не изобретать костыли с PECL-mcrypt.
- Переносимость данных между окружениями. OpenSSL доступен везде, включая PHP 5.3+. Если зашифрованные файлы должны читаться и на dev-сервере, и на проде, и у заказчика, OpenSSL надёжнее с точки зрения совместимости. Sodium-шифротекст расшифруется только там, где есть Sodium.
На практике мы используем OpenSSL на проектах, где важна совместимость между разными окружениями. Sodium, там, где весь стек обновлён до PHP 8 и безопасность на первом месте.
В видео Dave Hollingworth детально разбирает оба подхода с демонстрацией кода и объяснением криптографических примитивов за каждым из них. Материал дополняет статью: показаны edge-кейсы, установка библиотеки defuse/php-encryption через Composer и сравнение производительности OpenSSL и Sodium на реальных данных.
⁉️🤔 Частые вопросы
Можно ли использовать md5() или sha1() для шифрования файлов?
Нет.
md5()иsha1(), хеш-функции, они необратимы по определению. Зашифрованный файл должен расшифровываться обратно, а хеш расшифровать нельзя. Хеши применяются для проверки целостности (как HMAC в коде выше) и хранения паролей черезpassword_hash(), но не для шифрования содержимого.
Что делать, если ключ шифрования утерян?
Расшифровать данные без ключа невозможно. Храните ключ в
wp-config.phpвне document root и делайте его бэкап отдельно от бэкапов файлов и базы данных. Не кладите ключ в репозиторий Git, добавьтеwp-config.phpв.gitignoreили используйте переменные окружения.
Зачем нужен IV, если ключ и так секретный?
Без случайного IV одинаковые данные, зашифрованные одним ключом, дают одинаковый шифротекст. Злоумышленник, видя повторяющиеся блоки, получает информацию о структуре файла. IV делает каждый запуск шифрования уникальным: один и тот же файл, зашифрованный дважды с одним ключом, даёт два разных шифротекста.
Можно ли шифровать большие файлы, несколько гигабайт?
Функции выше читают файл в память целиком, для гигабайтных данных это приведёт к исчерпанию памяти. Для потокового шифрования используйте
openssl_encrypt()в цикле с буферизацией по чанкам (например, по 1 МБ) или библиотекуdefuse/php-encryption, которая поддерживает потоковый режим из коробки.
Работает ли этот код на PHP 8.3?
Да. И OpenSSL, и Sodium полностью поддерживаются в PHP 8. Код протестирован на актуальных версиях PHP и не использует deprecated-функций. На PHP 8.3 совместимость сохраняется, обратных несовместимостей в OpenSSL-расширении нет.
Шифрование файлов в 2026 году: практический вердикт
Mcrypt ушёл из PHP, и это к лучшему. Два встроенных инструмента на замену и безопаснее, и быстрее, и не требуют PECL-танцев с бубном. OpenSSL работает везде, Sodium проще и надёжнее по умолчанию.
Если коротко: новый проект на PHP 8, стартуйте с Sodium, код получается чище. Переносите старый код с mcrypt, перепишите на OpenSSL, он доступен даже на PHP 7.0. И главное правило криптографии: храните ключи отдельно от зашифрованных данных. Потеря ключа = потеря данных, и брутфорс тут не спасёт.
Начните с тестового скрипта на копии файла: убедитесь, что цикл «зашифровал → расшифровал → побайтово совпало» отрабатывает без ошибок. А какой способ используете вы на своих проектах, OpenSSL, Sodium или что-то ещё? Напишите в комментариях.



