ENES
hashingEngineering Guide

Hashing vs Encryption: SHA-256, Salting, and Rainbow Tables (2026)

AS
Published on 2026-10-19ยท8 min readยทDaily Toolbox Engineering

Hashing vs Encryption: SHA-256, Salting, and Rainbow Tables

"Should I hash it or encrypt it?" is one of the most consequential questions in application security, and the two are often confused because both turn readable data into unreadable bytes. But they solve opposite problems: hashing produces a one-way fingerprint you can verify but never reverse; encryption scrambles data so it can be unscrambled later with a key. Using the wrong one โ€” encrypting passwords, hashing credit card numbers you need to charge โ€” creates vulnerabilities that no amount of code review can fix afterward.

The Core Difference

A cryptographic hash function takes input of any size and produces a fixed-size digest. It is:

  • Deterministic โ€” the same input always gives the same digest.
  • One-way โ€” given the digest, you cannot recover the input (preimage resistance).
  • Collision-resistant โ€” you cannot feasibly find two inputs with the same digest.
  • Avalanching โ€” flipping one input bit changes roughly half the output bits.

Encryption takes a plaintext and a key and produces ciphertext that can be decrypted back to the plaintext with the (same or paired) key. It is reversible by design โ€” that's the point. If you encrypt passwords, anyone who steals the key (or finds it in your config, logs, or backups) recovers every password.

The decision rule is simple:

  • Need to verify data later without storing it? โ†’ Hash (passwords, file integrity, deduplication).
  • Need to read the data again later? โ†’ Encrypt (credit card numbers, PII at rest, messages in transit).
  • Need to prove data hasn't changed and know who vouches for it? โ†’ Hash + sign (HMAC, digital signatures).

How SHA-256 Works (Enough to Respect It)

SHA-256 (FIPS 180-4) digests any input into 256 bits (32 bytes, usually shown as 64 hex characters). The pipeline:

  1. Padding. The message is padded to a multiple of 512 bits: append a 1 bit, then zeros, then the original message length as a 64-bit integer. This guarantees the length is always part of what's hashed (blocking length-extension tricks of older designs).
  2. Block processing. The padded message is split into 512-bit blocks. Each block goes through 64 rounds of mixing with the running 256-bit state, using bitwise operations (AND, OR, XOR, rotations) and round constants derived from the fractional parts of cube roots of the first 64 primes.
  3. Output. After the last block, the 256-bit state is the digest.

You don't need to memorize the round function. What matters: every input bit influences every output bit (avalanche), and the best known attack is brute force โ€” 2^256 work, which is not happening. SHA-256 is not broken and has no practical collisions (unlike MD5 and SHA-1, both of which are broken for collision resistance and must not be used for security purposes).

$ echo -n "hello" | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
$ echo -n "hellp" | sha256sum
94856b1c8ff8c17a7f7d5cc9a5597cc701eb1e023e2a4a7f0d63f2d1d2f9e2

One letter changed; the digests share nothing. That's the avalanche effect.

Passwords: Hash, Salt, and Stretch

Storing sha256(password) is not password hashing โ€” it's a fast hash, and fast is the enemy. An attacker with your database can test billions of SHA-256 guesses per second on a single GPU. Two defenses, both mandatory:

Salting. A unique random salt per user, stored alongside the hash: hash = KDF(password, salt). Salts defeat rainbow tables โ€” precomputed tables mapping hashes back to common passwords. Without salts, one rainbow table cracks every unsalted SHA-256 password database instantly (identical passwords share identical hashes, so one table covers all users). With unique salts, the attacker must brute-force each password individually from scratch.

Stretching (work factor). Use a password hashing function โ€” bcrypt, scrypt, or Argon2id โ€” that is deliberately slow and memory-hard, with a tunable cost parameter. Bcrypt at cost 12 takes ~250ms per hash on modern hardware: imperceptible at login, devastating to brute force (billions of guesses become thousands).

import bcrypt

# Registration: hash with a fresh random salt, cost factor 12
password = b"correct horse battery staple"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# b'$2b$12$...'

# Login: constant-time comparison
if bcrypt.checkpw(password, hashed):
    print("authenticated")

The hash string embeds the algorithm, cost, and salt โ€” no separate salt column needed. Argon2id (winner of the 2015 Password Hashing Competition) is the current best practice for new systems; bcrypt remains perfectly respectable.

Key numbers to remember: a random 256-bit salt per password; cost tuned so hashing takes ~100โ€“500ms on your hardware; re-tune the cost upward every couple of years as hardware improves.

Rainbow Tables: Why Salts Killed Them

A rainbow table is a time-memory tradeoff: precompute chains of hash โ†’ reduce โ†’ hash โ†’ ... for a password space, store only chain endpoints, and invert any hash in that space in seconds. In the 2000s, tables for all 8-character alphanumeric passwords fit on a DVD.

Salts destroy this economics completely. A rainbow table is computed for one specific salt (or none). With a unique 128-bit salt per user, the attacker needs a separate table per user โ€” 2^128 tables โ€” which is another way of saying "impossible." This is why every modern password scheme salts: it converts a break-once-run-anywhere attack into per-password brute force.

Note what salts do not do: they don't make weak passwords strong. Password1! with a salt still falls to a dictionary attack in milliseconds. Salts force per-password work; password complexity (or better, passphrases and MFA) determines how much work that is.

Common Mistakes

1. Encrypting passwords. If the key leaks, every password is recoverable. Passwords are verified, not retrieved โ€” hash them.

2. Using fast hashes for passwords. MD5, SHA-1, SHA-256 are designed to be fast. Password hashing must be slow. Use bcrypt, scrypt, or Argon2id.

3. One global salt (or no salt). A single salt for all users is barely better than none โ€” identical passwords still share hashes, and one rainbow table still covers everyone.

4. Hashing PII you need to read back. Hashed emails can't be used for "log in with email" lookups... actually they can (hash the input and compare), but they can't be displayed or exported. If the business needs the original value, encrypt โ€” don't hash.

5. Rolling your own KDF. sha256(sha256(sha256(password))) iterated 1000 times is not bcrypt. Iteration counts, memory hardness, and side-channel resistance are subtle; use the standard algorithms.

6. Comparing hashes with ==. String comparison short-circuits on the first differing byte, leaking timing information. Use constant-time comparison (hmac.compare_digest, bcrypt.checkpw).

7. Forgetting pepper. A pepper is a secret salt stored separately from the database (e.g., in an HSM or config). If the DB leaks but the pepper doesn't, the hashes are useless. It's defense in depth on top of per-user salts โ€” not a replacement for them.

Debugging Walkthrough: "Logins Fail After the Migration"

  1. Check the encoding. Bcrypt has a 72-byte input limit โ€” passwords longer than 72 bytes are silently truncated. If the old system hashed the full password and the new one truncates (or vice versa), long passwords fail. (Argon2id has no such limit โ€” another reason to prefer it.)
  2. Verify the cost parameter parses. Some libraries choke on $2y$ vs $2b$ prefixes (different bcrypt versions). Normalize or re-hash on next login.
  3. Confirm the salt is per-user. Query the database: if every row's hash starts with the same 22 characters after $2b$12$, you have one global salt โ€” re-hash with unique salts.
  4. Check for double hashing. bcrypt(sha256(password)) during migration but plain bcrypt(password) at verification (or the reverse) fails silently. The pipeline must be identical at hash time and check time.
  5. Look at timing. If verification takes 5ms instead of 250ms, the cost factor isn't being applied โ€” you're probably comparing against a fast hash, not bcrypt.

Try It

Hashing is concrete โ€” run it and see. DailyToolbox's free hash generator runs entirely in your browser: compute SHA-256, SHA-512, and other digests of any input, compare avalanche effects, and verify checksums without uploading anything.

๐Ÿ‘‰ Generate a hash โ†’

FAQ

Is SHA-256 still secure?

Yes. No practical preimage or collision attacks exist against SHA-256. (MD5 and SHA-1 are broken for collisions โ€” don't use them for security.) The caution around SHA-256 is about misuse: it's too fast for password hashing and vulnerable to length-extension in naive MAC constructions โ€” use HMAC-SHA256 instead of SHA256(key || message).

What's the difference between a salt and a pepper?

A salt is unique per password, stored with the hash, and defeats rainbow tables. A pepper is a single secret value (same for all users) stored separately from the database โ€” it means a bare database leak isn't enough to start cracking. Use both: per-user salts always, pepper as defense in depth.

Can two different inputs really never have the same SHA-256 hash?

They can in theory (pigeonhole principle: infinite inputs, 2^256 outputs) but never in practice โ€” finding one would take ~2^128 work via the birthday bound, far beyond any feasible computation. For password storage this is a non-issue.

Why not just encrypt the database and store plaintext passwords?

Encryption protects data at rest โ€” stolen disk, lost backup. But the running application must decrypt to verify logins, so the key lives in memory next to the code that uses it. Anyone who compromises the app gets both. Hashing means the plaintext password never exists anywhere after registration โ€” there's nothing to steal.

How often should I increase the bcrypt cost factor?

Reassess every 1โ€“2 years or when you upgrade hardware. The target is ~100โ€“500ms per hash on your production machines. Most libraries make this easy: store the cost in the hash string, and when a user logs in with an old cost, re-hash with the new one transparently.

#hashing#encryption#sha-256#security#cryptography
AS
Written by Alex Sun

Alex Sun is the developer behind Daily Toolbox. He writes these guides while building the tools themselves โ€” every claim tested against the real thing.

Try the free tools mentioned above

Try the Hash Generator โ†’