PHPEncryption & Hashing

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.

Never encrypt a password, and never hash data you need back
Encrypting a password instead of hashing it means anyone who obtains both the ciphertext and the encryption key — a real possibility if the key lives in the same compromised environment as the data — can decrypt every password at once. Passwords should be hashed with `password_hash()`, covered on its own page, never encrypted. Going the other direction, hashing a value you actually need to read back later (say, a customer's mailing address) makes that data permanently unrecoverable, since there is no decrypt operation for a hash.
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
<?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
<?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
<?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.');
}
Use hash_equals() even for non-secret comparisons
The file-integrity example above still uses `hash_equals()` rather than `===` out of habit and consistency — it costs nothing extra here, and it removes any question of whether a given comparison is timing-sensitive. Reserve exact-match plain string comparisons for values that are never derived from user input or secrets.
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_secretbox and a securely stored key.

  • Use hash() for checksums and integrity checks on non-secret data, not for anything requiring secrecy.

  • Never use md5() or sha1() 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.

Tip
Keep encryption keys out of the codebase entirely — load them from an environment variable or a secrets manager, and rotate them periodically. A perfectly implemented encryption scheme provides no protection if the key sits in the same git repository as the ciphertext it protects.