Skip to content

Everything for WordPress, web development — and beyond

🔒 Encrypting and decrypting files in PHP: OpenSSL and Sodium instead of mcrypt

🔒 Encrypting and decrypting files in PHP: OpenSSL and Sodium instead of mcrypt

Encrypt a file before saving it to the server and be confident that nobody, not even the hosting administrator, can read it without the key. Sounds like a basic need, but with mcrypt leaving PHP, the familiar approach stopped working.

The mcrypt extension was marked deprecated in PHP 7.1 and completely removed in PHP 7.2 back in 2017. Today, on PHP 8, attempting to call mcrypt_encrypt() ends in a fatal error. Meanwhile, confidential files still sit on servers: database backups, CSVs with personal data, PDF contracts, order exports.

Good news: PHP provides two working mechanisms out of the box, OpenSSL and Sodium. Both require no additional extension installation on modern hosting, both are faster and more secure than mcrypt. Below is a practical guide to encrypting and decrypting files on PHP 8 with code you can copy and run.

💡 Quick overview:

  • Why mcrypt is dead and which PHP versions are affected
  • Step-by-step file encryption via OpenSSL with AES-256-CBC
  • Decryption with data tampering protection via HMAC
  • Sodium alternative for PHP 7.2 and higher
  • When to choose OpenSSL and when Sodium

Why mcrypt is no longer an option

The mcrypt library hasn't been updated since 2007. Critical vulnerabilities were found in its code, and no maintainers remained. The PHP team made a decision: in PHP 7.1 the extension was marked deprecated, and in PHP 7.2, released in November 2017, completely excluded from core.

If you're migrating an old project with mcrypt, check phpinfo(). On PHP 7.2+ there's no "mcrypt support: enabled" line. Calling mcrypt_encrypt(), mcrypt_decrypt() or stream filters mcrypt.tripledes / mdecrypt.tripledes returns "Call to undefined function" error.

Technically mcrypt is available via PECL with pecl install mcrypt command. But installing an unsupported extension with known vulnerabilities on a production server for one legacy script is a bad idea. Rewrite encryption with OpenSSL: it's built into PHP since version 5.3 and isn't going anywhere.

File encryption via OpenSSL, step by step

OpenSSL in PHP is represented by openssl_encrypt() and openssl_decrypt() functions. They work with raw data and support dozens of algorithms, from AES-128-CBC to AES-256-GCM. For files we use AES-256-CBC: it's cryptographically strong and doesn't require PHP 7.1 unlike GCM with additional tag parameters.

Step 1: generate encryption key

The key is the main secret of the entire scheme. It must be cryptographically random, not manually invented. No "secret-password" from examples, only openssl_random_pseudo_bytes().

The script below generates a 256-bit key and outputs it in format for insertion into wp-config.php. Run once via command line and save the result:

1<?php
2// Generating a random 256-bit key (32 bytes)
3$encryption_key = base64_encode(openssl_random_pseudo_bytes(32));
4echo "define('FILE_ENCRYPTION_KEY', '" . $encryption_key . "');\n";

The openssl_random_pseudo_bytes(32) function returns 32 bytes of cryptographically quality randomness. base64_encode converts binary data to a string that's convenient to store in configuration files. The key must be outside document root, in wp-config.php or .env, but not in theme code.

Step 2: file encryption function

The script reads a file from disk, encrypts with AES-256-CBC algorithm, adds a random IV at the beginning and HMAC signature for integrity verification, saves the result. Add the code to functions.php of a child theme or to a custom plugin.

Warning: before running on a production server, make a complete backup. Test encryption-decryption on a file copy in a test directory. If the key is lost, decrypting data is impossible, AES-256 cannot be brute-forced.

1<?php
2function 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}

What's happening here line by line:

  • openssl_cipher_iv_length('aes-256-cbc') returns 16, the length of the initialization vector for this algorithm.
  • openssl_random_pseudo_bytes($ivLength) creates a random IV. It makes identical data encrypted with the same key produce different ciphertext on each run.
  • OPENSSL_RAW_DATA tells the function to return binary data, not base64. We save raw ciphertext for compactness.
  • hash_hmac('sha256', ...) calculates a checksum from the IV-and-ciphertext bundle. During decryption we'll recalculate HMAC and compare: if data was modified or corrupted, the comparison won't match.
  • The file is saved in format: [IV 16 bytes][HMAC 32 bytes][ciphertext]. No delimiters, positions are fixed by lengths.

Step 3: decryption function

The reverse process: read IV, read HMAC, read ciphertext, recalculate HMAC and compare via hash_equals(), decrypt. Code is added to the same file:

1<?php
2function 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}

Key point: hash_equals() instead of ===. Regular string comparison is vulnerable to timing attacks, an attacker can pick HMAC byte-by-byte by measuring server response time. hash_equals() compares strings in constant time regardless of which character they diverged at.

Usage example with real paths

1<?php
2$key = FILE_ENCRYPTION_KEY; // from wp-config.php
3
4// Encrypting the database backup
5encrypt_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);
13header('Content-Type: application/octet-stream');
14header('Content-Disposition: attachment; filename="backup.sql"');
15echo $decrypted;

The functions are universal: they work with any file type, images, PDF, CSV, SQL dumps. Size is limited only by available RAM, since the file is read into memory entirely. For gigabyte files streaming processing with chunk buffering will be needed, but for the overwhelming majority of practical tasks this code is sufficient.

Alternative: Sodium (libsodium)

The Sodium extension is built into PHP starting from version 7.2 and became part of core in PHP 8.1. It provides a simpler API compared to OpenSSL: no need to manually manage IV and HMAC, authenticated encryption works out of the box.

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}

Note: Sodium verifies integrity itself. If data was modified or the key is wrong, sodium_crypto_secretbox_open() simply returns false, no separate HMAC checks required. The code is half as long.

One disadvantage: Sodium requires PHP 7.2 or higher. If the project runs on PHP 7.0-7.1, the only option is OpenSSL. But in practice in 2026, finding hosting with PHP below 7.4 is already difficult: according to WordPress.org data for June 2026, the share of PHP 7.0-7.1 is less than 0.3% of all installations.

OpenSSL or Sodium: two selection criteria

The choice between the two approaches comes down to two factors:

  • PHP version. PHP 7.2+, take Sodium, it's safer by default and doesn't let you mess up HMAC implementation. PHP 7.0-7.1, OpenSSL only. Below PHP 7.0, it's time to update the server, not invent workarounds with PECL-mcrypt.
  • Data portability between environments. OpenSSL is available everywhere, including PHP 5.3+. If encrypted files must be readable on dev server, on production, and at the client's, OpenSSL is more reliable from a compatibility standpoint. Sodium ciphertext will decrypt only where Sodium exists.

In practice we use OpenSSL on projects where compatibility between different environments is important. Sodium where the entire stack is updated to PHP 8 and security comes first.

In the video Dave Hollingworth breaks down both approaches in detail with code demonstration and explanation of cryptographic primitives behind each. The material complements the article: edge cases are shown, installation of defuse/php-encryption library via Composer and performance comparison of OpenSSL and Sodium on real data.

⁉️🤔 Frequently asked questions

Can I use md5() or sha1() for file encryption?

No. md5() and sha1() are hash functions, they're irreversible by definition. An encrypted file must decrypt back, and a hash cannot be decrypted. Hashes are used for integrity verification (like HMAC in the code above) and password storage via password_hash(), but not for content encryption.

What to do if the encryption key is lost?

Decrypting data without the key is impossible. Store the key in wp-config.php outside document root and back it up separately from file and database backups. Don't put the key in a Git repository, add wp-config.php to .gitignore or use environment variables.

Why is IV needed if the key is already secret?

Without a random IV, identical data encrypted with the same key produces identical ciphertext. An attacker seeing repeating blocks gets information about file structure. IV makes each encryption run unique: the same file encrypted twice with one key produces two different ciphertexts.

Can I encrypt large files, several gigabytes?

The functions above read the file into memory entirely, for gigabyte data this will lead to memory exhaustion. For streaming encryption use openssl_encrypt() in a loop with chunk buffering (for example, 1 MB each) or the defuse/php-encryption library, which supports streaming mode out of the box.

Does this code work on PHP 8.3?

Yes. Both OpenSSL and Sodium are fully supported in PHP 8. The code is tested on current PHP versions and doesn't use deprecated functions. On PHP 8.3 compatibility is maintained, there are no backwards incompatibilities in the OpenSSL extension.

File encryption in 2026: practical verdict

Mcrypt left PHP, and that's for the better. Two built-in replacement tools are both safer and faster and don't require PECL dance with a tambourine. OpenSSL works everywhere, Sodium is simpler and more reliable by default.

In short: new project on PHP 8, start with Sodium, the code turns out cleaner. Migrating old code from mcrypt, rewrite to OpenSSL, it's available even on PHP 7.0. And the main rule of cryptography: store keys separately from encrypted data. Key loss equals data loss, and brute force won't help here.

Start with a test script on a file copy: make sure the cycle "encrypted → decrypted → byte-by-byte matched" works without errors. And which method do you use on your projects, OpenSSL, Sodium or something else? Write in the comments.