Skip to content

Все для WordPress, веб-розробки — і не тільки

🔒 Шифрування та дешифрування файлів у PHP: OpenSSL і Sodium замість mcrypt

🔒 Шифрування та дешифрування файлів у 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));
4echo "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
2function 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
2function 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// Шифруем бэкап базы данных
5encrypt_file(
6 __DIR__ . '/backup.sql',
7 __DIR__ . '/backup.sql.enc',
8 $key
9);
10
11// Расшифровываем и отдаём на скачивание
12$decrypted = decrypt_file(__DIR__ . '/backup.sql.enc', $key);
13header('Content-Type: application/octet-stream');
14header('Content-Disposition: attachment; filename="backup.sql"');
15echo $decrypted;

Функції універсальні: працюють із будь-яким типом файлів, зображення, PDF, CSV, SQL-дампи. Розмір обмежений лише доступною оперативною пам'яттю, оскільки файл читається в пам'ять цілком. Для гігабайтних файлів знадобиться потокова обробка з буферизацією чанків, але для переважної більшості практичних задач цього коду достатньо.

Альтернатива: Sodium (libsodium)

Розширення Sodium вбудоване в PHP починаючи з версії 7.2 і стало частиною ядра в PHP 8.1. Воно надає простіший API порівняно з OpenSSL: не потрібно вручну керувати IV і HMAC, автентифіковане шифрування працює з коробки.

1<?php
2function 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
10function 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 чи щось інше? Напишіть у коментарях.