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?
| Algorithm | Use for passwords? |
|---|---|
| MD5, SHA-1 | No: broken and fast |
| SHA-256 / SHA-512 (plain or salted) | No: too fast for passwords (fine for file checksums and signatures) |
| BCrypt | Yes: Spring Security default, cost 10 by default |
| Argon2id, SCrypt | Yes: memory-hard, best current choice (needs Bouncy Castle) |
| PBKDF2 | Yes: 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.