Passwords and Hashing

Never Store Plain Passwords

If the database leaks (it happens to big companies every year), plain-text passwords expose every user, including on other sites where they reused the password. So we store a hash: a one-way fingerprint of the password.

  • Encryption is two-way: with the key you get the original back. Wrong tool for passwords.
  • Hashing is one-way: you can compute hash(password) but not reverse it. At login you hash the typed password and compare.
  • Salt: a random value stored next to the hash and mixed in, so equal passwords get different hashes.
  • Slow (work factor): password hashes like BCrypt are deliberately slow so guessing billions of passwords is impractical.

SHA-256 vs Salted Hashing

Example

Plain SHA-256 gives the same hash for the same password, so two users with password123 are easy to spot and a precomputed "rainbow table" cracks them instantly. A per-user salt fixes that:

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;

public class Main {
    static String sha256(byte[] salt, String password) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        if (salt != null) md.update(salt);
        byte[] hash = md.digest(password.getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(hash).substring(0, 24) + "...";
    }

    public static void main(String[] args) throws Exception {
        System.out.println("Unsalted SHA-256:");
        System.out.println("  asha  password123 -> " + sha256(null, "password123"));
        System.out.println("  ravi  password123 -> " + sha256(null, "password123"));

        // In real code the salt comes from SecureRandom; fixed bytes keep this demo's output stable.
        byte[] ashaSalt = HexFormat.of().parseHex("9f2c4a7e11d05b83");
        byte[] raviSalt = HexFormat.of().parseHex("3b8e60d2a4f71c95");
        System.out.println("Salted SHA-256:");
        System.out.println("  asha  password123 -> " + sha256(ashaSalt, "password123"));
        System.out.println("  ravi  password123 -> " + sha256(raviSalt, "password123"));

        // Login check: recompute with the stored salt and compare
        String stored = sha256(ashaSalt, "password123");
        System.out.println("Login with password123: " + stored.equals(sha256(ashaSalt, "password123")));
        System.out.println("Login with Password123: " + stored.equals(sha256(ashaSalt, "Password123")));
    }
}

Output

Unsalted SHA-256:
  asha  password123 -> ef92b778bafe771e89245b89...
  ravi  password123 -> ef92b778bafe771e89245b89...
Salted SHA-256:
  asha  password123 -> 4e2a99e328f2622c8d951db6...
  ravi  password123 -> a7939a237ac654cc5f590d6f...
Login with password123: true
Login with Password123: false

Salt Is Not Enough: Make It Slow

Example

SHA-256 is designed to be fast; a GPU computes billions per second. Password hashing functions repeat the work thousands of times. PBKDF2 (built into the JDK) shows the idea; BCrypt (Spring Security default) works the same way with a "cost" setting:

import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class Main {
    static byte[] pbkdf2(String password, byte[] salt, int iterations) throws Exception {
        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 256);
        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
    }

    public static void main(String[] args) throws Exception {
        byte[] salt = HexFormat.of().parseHex("9f2c4a7e11d05b83a0c1e2f3");
        int iterations = 100_000;

        byte[] stored = pbkdf2("password123", salt, iterations);
        String record = "pbkdf2$" + iterations + "$" + HexFormat.of().formatHex(salt) + "$"
                + HexFormat.of().formatHex(stored).substring(0, 20) + "...";
        System.out.println("Stored in DB: " + record);

        for (String attempt : new String[] {"password123", "password124"}) {
            byte[] check = pbkdf2(attempt, salt, iterations);
            // MessageDigest.isEqual compares in constant time (no timing leak)
            System.out.println(attempt + " matches: " + MessageDigest.isEqual(stored, check));
        }
        System.out.println("Each guess costs an attacker " + iterations + " HMAC rounds instead of 1");
    }
}

Output

Stored in DB: pbkdf2$100000$9f2c4a7e11d05b83a0c1e2f3$d116548ee1331e5cc4b6...
password123 matches: true
password124 matches: false
Each guess costs an attacker 100000 HMAC rounds instead of 1

Tip: You will not write hashing code yourself in Spring apps: BCryptPasswordEncoder does salt + cost for you. These demos show why it is built that way.

Which Algorithm?

AlgorithmUse for passwords?
MD5, SHA-1No: broken and fast
SHA-256 / SHA-512 (plain or salted)No: too fast for passwords (fine for file checksums and signatures)
BCryptYes: Spring Security default, cost 10 by default
Argon2id, SCryptYes: memory-hard, best current choice (needs Bouncy Castle)
PBKDF2Yes: when FIPS compliance is required

Exercise: Two users have the same BCrypt-hashed password. Will their hashes in the database be equal? Why?

Show answer
No. BCrypt generates a new random salt for every hash and stores it inside the hash string,
so the same password gives a different hash each time.