Java: storing passwords with a salt and PBKDF2
Derive a password hash with a random salt and many iterations, then verify a login attempt.
On this page
Java byte arrays and SHA-256 hashing.
- Derive a password hash with a random salt and many iterations, then verify a login attempt.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A site should never store passwords in plain text, and should not store a plain fast hash either. A password hashing function is deliberately slow and takes a salt, a random value stored beside the hash. The salt makes identical passwords produce different hashes and defeats precomputed tables.
PBKDF2 repeats an HMAC many times, which makes each guess expensive. To verify a login, derive the hash again with the stored salt and compare it with the stored value using a constant-time comparison. Current guidance, for example the OWASP Password Storage Cheat Sheet, asks for hundreds of thousands of iterations for PBKDF2 with HMAC-SHA-256. Newer designs such as Argon2id or scrypt are preferred when available.
Code example
import java.security.MessageDigest;
import java.security.SecureRandom;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
class Main {
static byte[] derive(char[] password, byte[] salt) throws Exception {
PBEKeySpec spec = new PBEKeySpec(password, salt, 600_000, 256);
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
}
public static void main(String[] args) throws Exception {
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
byte[] stored = derive("correct horse".toCharArray(), salt);
byte[] good = derive("correct horse".toCharArray(), salt);
byte[] bad = derive("wrong guess".toCharArray(), salt);
System.out.println("hash length: " + stored.length + " bytes");
System.out.println("right password: " + MessageDigest.isEqual(stored, good));
System.out.println("wrong password: " + MessageDigest.isEqual(stored, bad));
}
}hash length: 32 bytes right password: true wrong password: false
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
import java.security.MessageDigest;import java.security.SecureRandom;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec; class Main { static byte[] derive(char[] password, byte[] salt) throws Exception { PBEKeySpec spec = new PBEKeySpec(password, salt, 600_000, 256); return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded(); } public static void main(String[] args) throws Exception { byte[] salt = new byte[16]; new SecureRandom().nextBytes(salt); byte[] stored = derive("correct horse".toCharArray(), salt); byte[] good = derive("correct horse".toCharArray(), salt); byte[] bad = derive("wrong guess".toCharArray(), salt); System.out.println("hash length: " + stored.length + " bytes"); System.out.println("right password: " + MessageDigest.isEqual(stored, good)); System.out.println("wrong password: " + MessageDigest.isEqual(stored, bad)); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Fill the salt with 16 cryptographically secure random bytes. | salt = 16 random bytes | — | |
| Store the salt and this 256-bit hash, never the password itself. | stored = 32 bytes | — | |
| At login, derive again with the stored salt and the typed password. | same input and salt give the same hash | — | |
| A different password gives a different hash, so the login fails. | same input and salt give the same hash | hash length: 32 bytes right password: true wrong password: false |
Behind the scenes
How Java processes this code
The Java compiler produces bytecode. The JVM loads the class and invokes its main method.
Cost and common mistakes
Each derivation costs O(iterations); the cost is chosen to be high on purpose.
- Use SecureRandom, not java.util.Random, to make a salt.
- Compare hashes in constant time. A normal equals can leak timing information.
- Store the algorithm, iteration count, salt, and hash together so the cost can be raised later.
Try it yourself
Why is a different salt needed for every user?
Reveal answer and reasoning
With a shared salt, two users with the same password would have the same hash and one cracked guess would reveal both. A unique salt forces each hash to be attacked separately.