Encryption & Hashing in PHP
"Hashing" and "encryption" get used interchangeably in casual conversation, but they solve two different problems and are not interchangeable in code. Mixing them up — encrypting a password instead of hashing it, or hashing data you actually need to read back later — leads to real security bugs, so it is worth being precise about which one a given piece of data needs.
Hashing vs encryption: the core distinction
Hashing is one-way. It takes an input of any size and produces a fixed-size output, and there is no operation that reverses a hash back into the original input — the only way to "check" a hash is to hash a candidate value again and compare the results. That is exactly the property passwords need: your application should never be able to recover a user's actual password from what is stored, so that a database leak does not expose real credentials. Encryption is two-way. It transforms data into ciphertext using a key, and that exact process can be reversed with the matching key to recover the original data. Encryption is what you reach for when your application genuinely needs the original value back later — a stored credit card number, a government ID, or any field a legitimate feature has to redisplay or process in its original form.
Symmetric encryption with sodium_crypto_secretbox
Since PHP 7.2, the Sodium extension (libsodium) ships with PHP by default, which means every modern PHP installation has access to well-vetted, modern encryption without installing a separate library. sodium_crypto_secretbox() performs symmetric encryption — the same secret key both encrypts and decrypts the data — using an algorithm (XSalsa20-Poly1305) that also authenticates the ciphertext, so tampering with the encrypted data is detected rather than silently producing garbage on decrypt.
Encrypting a value
<?php // Generate this once and store it outside the codebase (env var, secret manager) — // never hardcode it, and never commit it to version control. $key = sodium_crypto_secretbox_keygen(); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $plaintext = 'credit-card-token-or-other-sensitive-value'; $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key); // store both $nonce and $ciphertext — the nonce is not secret, // but it must never be reused with the same key $stored = base64_encode($nonce . $ciphertext);
Decrypting the value
<?php
$raw = base64_decode($stored);
$nonce = mb_substr($raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
if ($plaintext === false) {
// authentication failed — data was tampered with, or the wrong key/nonce was used
}A nonce ("number used once") has to be different for every message encrypted under the same key — random_bytes() generates one that is large enough to make an accidental collision astronomically unlikely. The nonce does not need to be kept secret, so it is normal and safe to store it alongside the ciphertext, exactly as the example above does.
hash() for checksums, not passwords
PHP's general-purpose hash() function still has a legitimate place — just not for passwords. It is the right tool when you need a fixed-size fingerprint of data to detect changes: verifying a downloaded file was not corrupted or tampered with, generating a cache key from some input, or deduplicating identical uploads. These are integrity checks, not secrets a determined attacker is trying to reverse, so hash() being fast is actually desirable here rather than a weakness.
Verifying file integrity with a checksum
<?php
$expectedChecksum = 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3';
$actualChecksum = hash('sha256', file_get_contents('downloaded-package.zip'));
if (!hash_equals($expectedChecksum, $actualChecksum)) {
throw new RuntimeException('Downloaded file failed integrity check.');
}Why md5() and sha1() are unsuitable for either job
md5() and sha1() are both fast general-purpose hash functions, and speed is precisely the wrong property for anything security-sensitive. For passwords, that speed lets an attacker try billions of guesses per second against a leaked table, which is why password_hash() exists and deliberately uses slow, tunable algorithms instead. For encryption, md5() and sha1() are not encryption algorithms at all — they have no way to reverse the output back to the input, so they cannot serve that purpose regardless of speed. Their remaining legitimate use is non-security checksums, and even there, sha256 (via hash('sha256', ...)) is generally preferred today since both md5 and sha1 have known collision weaknesses.
sha256 checksum: 2c624232cdd221771294dfbb310aca000a0df6ac8b66b696d90ef06fdefb64a
Hash passwords (one-way, irreversible) with
password_hash()— never encrypt them.Encrypt data you need to read back later (two-way, reversible) with
sodium_crypto_secretboxand a securely stored key.Use
hash()for checksums and integrity checks on non-secret data, not for anything requiring secrecy.Never use
md5()orsha1()for passwords or for anything else security-sensitive.Never reuse a nonce with the same encryption key, and never hardcode encryption keys in source code.