Introduction to Password Hashing
Password hashing is critical for securing user credentials. Bcrypt and PBKDF2 are two of the most widely used password hashing algorithms, each with different design goals and security characteristics.
Comparison Overview
| Feature | Bcrypt | PBKDF2 |
|---|---|---|
| Design | Blowfish-based | HMAC-based |
| Output | 60 characters (modular crypt format) | Configurable |
| Salt | Automatic, embedded in hash | Separate, must be stored |
| Work Factor | Cost parameter (2^cost iterations) | Iteration count |
| Memory Hard | Yes (4KB internal state) | No |
| GPU Resistant | Moderately | Poor |
| Parallelization | Limited | Highly parallelizable |
| Standard | OpenBSD, PHC finalist | NIST, RFC 2898 |
| Java Support | jBCrypt, Spring Security | Built-in (SecretKeyFactory) |
Architecture Overview
Password Hashing Architecture ├── Bcrypt │ ├ - Blowfish Key Schedule (4KB memory) │ ├ - EksBlowfishSetup (expensive key setup) │ ├ - 64 rounds of Blowfish encryption │ └ - Modular Crypt Format: $2a$10$salt$hash ├── PBKDF2 │ ├ - HMAC (SHA1, SHA256, SHA512) │ ├ - Iterations (configurable) │ ├ - Salt (must be stored separately) │ └ - Raw bytes or hex encoding └── Application Layer ├ - Password Validation ├ - Work Factor Management ├ - Legacy Migration └ - Security Monitoring
Core Implementation
1. Maven Dependencies
<properties>
<spring-security.version>6.2.0</spring-security.version>
<bcrypt.version>0.10.2</bcrypt.version>
<bouncycastle.version>1.78</bouncycastle.version>
</properties>
<dependencies>
<!-- Spring Security Crypto (includes bcrypt) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- jBCrypt (pure Java implementation) -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>${bcrypt.version}</version>
</dependency>
<!-- Bouncy Castle for additional PBKDF2 algorithms -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- Apache Commons Codec for encoding -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<!-- Guava for utilities -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.0.0-jre</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
2. Bcrypt Implementation
package com.password.hashing.bcrypt;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@Service
public class BcryptService {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// BCrypt versions
public enum BcryptVersion {
VERSION_2A("2a"), // Original
VERSION_2B("2b"), // Fixed bug with UTF-8 handling
VERSION_2Y("2y"); // BSD systems
final String prefix;
BcryptVersion(String prefix) {
this.prefix = prefix;
}
public String getPrefix() { return prefix; }
}
// BCrypt parameters
public static class BcryptParams {
private final int cost;
private final BcryptVersion version;
private final byte[] salt;
public BcryptParams(int cost, BcryptVersion version) {
this.cost = cost;
this.version = version;
this.salt = BCrypt.gensalt(cost).getBytes();
}
public int getCost() { return cost; }
public BcryptVersion getVersion() { return version; }
public byte[] getSalt() { return salt; }
public String getSaltString() {
return new String(salt);
}
}
/**
* Hash password using jBCrypt
*/
public String hashPassword(String password, int cost) {
if (cost < 4 || cost > 31) {
throw new IllegalArgumentException("Cost must be between 4 and 31");
}
long startTime = System.nanoTime();
String salt = BCrypt.gensalt(cost);
String hashed = BCrypt.hashpw(password, salt);
long duration = System.nanoTime() - startTime;
// Log performance for monitoring
logPerformance("jBCrypt", cost, duration);
return hashed;
}
/**
* Hash password using Spring Security's BCrypt
*/
public String hashPasswordSpring(String password, int strength) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(strength);
long startTime = System.nanoTime();
String hashed = encoder.encode(password);
long duration = System.nanoTime() - startTime;
logPerformance("Spring BCrypt", strength, duration);
return hashed;
}
/**
* Verify password against hash
*/
public boolean verifyPassword(String password, String hash) {
if (hash == null || hash.length() < 60) {
return false;
}
try {
return BCrypt.checkpw(password, hash);
} catch (Exception e) {
return false;
}
}
/**
* Verify with constant-time comparison
*/
public boolean verifyPasswordConstantTime(String password, String hash) {
if (hash == null || hash.length() < 60) {
return false;
}
try {
String computedHash = BCrypt.hashpw(password, hash);
return constantTimeEquals(computedHash, hash);
} catch (Exception e) {
return false;
}
}
/**
* Extract parameters from hash
*/
public BcryptParams extractParams(String hash) {
if (hash == null || !hash.startsWith("$2")) {
throw new IllegalArgumentException("Invalid BCrypt hash format");
}
// Format: $2a$10$salt$hash
String[] parts = hash.split("\\$");
if (parts.length < 4) {
throw new IllegalArgumentException("Invalid BCrypt hash format");
}
String versionStr = parts[1];
BcryptVersion version;
switch (versionStr) {
case "2a": version = BcryptVersion.VERSION_2A; break;
case "2b": version = BcryptVersion.VERSION_2B; break;
case "2y": version = BcryptVersion.VERSION_2Y; break;
default: throw new IllegalArgumentException("Unknown BCrypt version: " + versionStr);
}
int cost = Integer.parseInt(parts[2]);
String saltWithHash = parts[3];
String salt = saltWithHash.substring(0, 22);
return new BcryptParams(cost, version);
}
/**
* Benchmark BCrypt with different cost factors
*/
public Map<Integer, Long> benchmarkCosts(int minCost, int maxCost, String password) {
Map<Integer, Long> results = new HashMap<>();
for (int cost = minCost; cost <= maxCost; cost++) {
long totalTime = 0;
int iterations = 5;
for (int i = 0; i < iterations; i++) {
long start = System.nanoTime();
hashPassword(password, cost);
totalTime += System.nanoTime() - start;
}
long avgTimeMs = TimeUnit.NANOSECONDS.toMillis(totalTime / iterations);
results.put(cost, avgTimeMs);
System.out.printf("Cost %2d: %d ms%n", cost, avgTimeMs);
}
return results;
}
/**
* Migrate hash to stronger cost
*/
public String migrateHash(String password, String oldHash, int newCost) {
BcryptParams params = extractParams(oldHash);
if (params.getCost() < newCost) {
return hashPassword(password, newCost);
}
return oldHash;
}
/**
* Check if hash needs rehash
*/
public boolean needsRehash(String hash, int targetCost) {
try {
BcryptParams params = extractParams(hash);
return params.getCost() < targetCost;
} catch (Exception e) {
return true;
}
}
/**
* Generate salt for BCrypt
*/
public String generateSalt(int cost) {
return BCrypt.gensalt(cost);
}
/**
* Create custom hash with specific version
*/
public String hashWithVersion(String password, int cost, BcryptVersion version) {
String salt = String.format("$%s$%02d$", version.getPrefix(), cost) +
generateRandomSalt(22);
return BCrypt.hashpw(password, salt);
}
private String generateRandomSalt(int length) {
byte[] salt = new byte[length];
SECURE_RANDOM.nextBytes(salt);
return new String(salt);
}
private boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
private void logPerformance(String algorithm, int cost, long durationNanos) {
long durationMs = TimeUnit.NANOSECONDS.toMillis(durationNanos);
// Log to monitoring system
System.out.printf("%s cost=%d took %d ms%n", algorithm, cost, durationMs);
}
// Inner class for hash info
public static class BcryptHashInfo {
private final String hash;
private final int cost;
private final BcryptVersion version;
private final String salt;
public BcryptHashInfo(String hash, int cost, BcryptVersion version, String salt) {
this.hash = hash;
this.cost = cost;
this.version = version;
this.salt = salt;
}
public String getHash() { return hash; }
public int getCost() { return cost; }
public BcryptVersion getVersion() { return version; }
public String getSalt() { return salt; }
}
}
3. PBKDF2 Implementation
package com.password.hashing.pbkdf2;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class PBKDF2Service {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// PBKDF2 algorithms
public enum PBKDF2Algorithm {
PBKDF2_WITH_HMAC_SHA1("PBKDF2WithHmacSHA1", 160),
PBKDF2_WITH_HMAC_SHA256("PBKDF2WithHmacSHA256", 256),
PBKDF2_WITH_HMAC_SHA512("PBKDF2WithHmacSHA512", 512);
final String algorithm;
final int hashLength;
PBKDF2Algorithm(String algorithm, int hashLength) {
this.algorithm = algorithm;
this.hashLength = hashLength;
}
public String getAlgorithm() { return algorithm; }
public int getHashLength() { return hashLength; }
}
// PBKDF2 parameters
public static class PBKDF2Params {
private final byte[] salt;
private final int iterations;
private final int keyLength;
private final PBKDF2Algorithm algorithm;
public PBKDF2Params(byte[] salt, int iterations, int keyLength, PBKDF2Algorithm algorithm) {
this.salt = salt;
this.iterations = iterations;
this.keyLength = keyLength;
this.algorithm = algorithm;
}
public byte[] getSalt() { return salt; }
public int getIterations() { return iterations; }
public int getKeyLength() { return keyLength; }
public PBKDF2Algorithm getAlgorithm() { return algorithm; }
}
/**
* Hash password using PBKDF2
*/
public PBKDF2Hash hashPassword(String password,
PBKDF2Algorithm algorithm,
int iterations,
int keyLength) {
// Generate salt
byte[] salt = generateSalt(16); // 128-bit salt
long startTime = System.nanoTime();
try {
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt,
iterations,
keyLength
);
SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm.getAlgorithm());
byte[] hash = factory.generateSecret(spec).getEncoded();
long duration = System.nanoTime() - startTime;
logPerformance(algorithm, iterations, duration);
return new PBKDF2Hash(hash, salt, iterations, algorithm);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException("PBKDF2 hashing failed", e);
} finally {
// Clear sensitive data
// Note: password char array can't be cleared easily here
}
}
/**
* Hash with automatic iteration tuning
*/
public PBKDF2Hash hashPasswordWithTargetTime(String password,
PBKDF2Algorithm algorithm,
int targetTimeMs,
int keyLength) {
// Find iterations that achieve target time
int iterations = findOptimalIterations(password, algorithm, targetTimeMs);
return hashPassword(password, algorithm, iterations, keyLength);
}
/**
* Verify password against PBKDF2 hash
*/
public boolean verifyPassword(String password, PBKDF2Hash hash) {
try {
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
hash.getSalt(),
hash.getIterations(),
hash.getHash().length * 8
);
SecretKeyFactory factory = SecretKeyFactory.getInstance(hash.getAlgorithm().getAlgorithm());
byte[] computedHash = factory.generateSecret(spec).getEncoded();
return constantTimeEquals(computedHash, hash.getHash());
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return false;
}
}
/**
* Verify with format string
*/
public boolean verifyPassword(String password, String hashString) {
PBKDF2Hash hash = PBKDF2Hash.fromString(hashString);
return verifyPassword(password, hash);
}
/**
* Benchmark PBKDF2 with different iteration counts
*/
public Map<Integer, Long> benchmarkIterations(PBKDF2Algorithm algorithm,
String password,
int minIterations,
int maxIterations,
int step) {
Map<Integer, Long> results = new HashMap<>();
for (int iterations = minIterations; iterations <= maxIterations; iterations += step) {
long totalTime = 0;
int samples = 3;
for (int i = 0; i < samples; i++) {
long start = System.nanoTime();
hashPassword(password, algorithm, iterations, 256);
totalTime += System.nanoTime() - start;
}
long avgTimeMs = TimeUnit.NANOSECONDS.toMillis(totalTime / samples);
results.put(iterations, avgTimeMs);
System.out.printf("Iterations %,7d: %d ms%n", iterations, avgTimeMs);
}
return results;
}
/**
* Find optimal iterations for target time
*/
public int findOptimalIterations(String password,
PBKDF2Algorithm algorithm,
int targetTimeMs) {
int iterations = 10000; // Start with 10k
int maxIterations = 10_000_000;
while (iterations < maxIterations) {
long start = System.nanoTime();
hashPassword(password, algorithm, iterations, 256);
long duration = System.nanoTime() - start;
long durationMs = TimeUnit.NANOSECONDS.toMillis(duration);
if (durationMs >= targetTimeMs) {
return iterations;
}
// Increase iterations proportionally
double ratio = (double) targetTimeMs / Math.max(durationMs, 1);
iterations = (int) (iterations * ratio * 0.9); // 10% safety margin
}
return maxIterations;
}
/**
* Migrate hash to stronger parameters
*/
public PBKDF2Hash migrateHash(String password,
PBKDF2Hash oldHash,
int newIterations,
int newKeyLength) {
if (oldHash.getIterations() < newIterations ||
oldHash.getHash().length * 8 < newKeyLength) {
return hashPassword(password, oldHash.getAlgorithm(), newIterations, newKeyLength);
}
return oldHash;
}
/**
* Check if hash needs rehash
*/
public boolean needsRehash(PBKDF2Hash hash, int targetIterations, int targetKeyLength) {
return hash.getIterations() < targetIterations ||
hash.getHash().length * 8 < targetKeyLength;
}
/**
* Generate salt
*/
public byte[] generateSalt(int length) {
byte[] salt = new byte[length];
SECURE_RANDOM.nextBytes(salt);
return salt;
}
/**
* Format hash as string (similar to Modular Crypt Format)
*/
public String formatHash(PBKDF2Hash hash) {
// Format: $pbkdf2-alg$iterations$salt$hash
String algorithm = hash.getAlgorithm().name().toLowerCase();
String saltB64 = Base64.getEncoder().encodeToString(hash.getSalt());
String hashB64 = Base64.getEncoder().encodeToString(hash.getHash());
return String.format("$%s$%d$%s$%s",
algorithm,
hash.getIterations(),
saltB64,
hashB64
);
}
/**
* Parse formatted hash
*/
public PBKDF2Hash parseHash(String formattedHash) {
String[] parts = formattedHash.split("\\$");
if (parts.length < 5) {
throw new IllegalArgumentException("Invalid hash format");
}
String algorithmStr = parts[1].toUpperCase();
PBKDF2Algorithm algorithm = PBKDF2Algorithm.valueOf(algorithmStr);
int iterations = Integer.parseInt(parts[2]);
byte[] salt = Base64.getDecoder().decode(parts[3]);
byte[] hash = Base64.getDecoder().decode(parts[4]);
return new PBKDF2Hash(hash, salt, iterations, algorithm);
}
private boolean constantTimeEquals(byte[] a, byte[] b) {
if (a.length != b.length) {
return false;
}
int result = 0;
for (int i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result == 0;
}
private void logPerformance(PBKDF2Algorithm algorithm, int iterations, long durationNanos) {
long durationMs = TimeUnit.NANOSECONDS.toMillis(durationNanos);
System.out.printf("%s iterations=%,d took %d ms%n",
algorithm.getAlgorithm(), iterations, durationMs);
}
// Data class for PBKDF2 hash
public static class PBKDF2Hash {
private final byte[] hash;
private final byte[] salt;
private final int iterations;
private final PBKDF2Algorithm algorithm;
public PBKDF2Hash(byte[] hash, byte[] salt, int iterations, PBKDF2Algorithm algorithm) {
this.hash = hash;
this.salt = salt;
this.iterations = iterations;
this.algorithm = algorithm;
}
public byte[] getHash() { return hash; }
public byte[] getSalt() { return salt; }
public int getIterations() { return iterations; }
public PBKDF2Algorithm getAlgorithm() { return algorithm; }
public String toStorageFormat() {
return String.format("%s:%d:%s:%s",
algorithm.name(),
iterations,
Base64.getEncoder().encodeToString(salt),
Base64.getEncoder().encodeToString(hash)
);
}
public static PBKDF2Hash fromString(String str) {
String[] parts = str.split(":");
PBKDF2Algorithm algorithm = PBKDF2Algorithm.valueOf(parts[0]);
int iterations = Integer.parseInt(parts[1]);
byte[] salt = Base64.getDecoder().decode(parts[2]);
byte[] hash = Base64.getDecoder().decode(parts[3]);
return new PBKDF2Hash(hash, salt, iterations, algorithm);
}
}
}
4. Hybrid Password Hashing Service
package com.password.hashing;
import com.password.hashing.bcrypt.BcryptService;
import com.password.hashing.pbkdf2.PBKDF2Service;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Service
public class HybridPasswordHashingService {
private final BcryptService bcryptService;
private final PBKDF2Service pbkdf2Service;
public enum HashAlgorithm {
BCRYPT,
PBKDF2_SHA256,
PBKDF2_SHA512
}
public HybridPasswordHashingService(BcryptService bcryptService,
PBKDF2Service pbkdf2Service) {
this.bcryptService = bcryptService;
this.pbkdf2Service = pbkdf2Service;
}
/**
* Hash password with selected algorithm
*/
public HashResult hashPassword(String password,
HashAlgorithm algorithm,
HashParameters parameters) {
switch (algorithm) {
case BCRYPT:
String bcryptHash = bcryptService.hashPassword(
password,
parameters.getBcryptCost()
);
return new HashResult(algorithm, bcryptHash);
case PBKDF2_SHA256:
PBKDF2Service.PBKDF2Hash hash256 = pbkdf2Service.hashPassword(
password,
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256,
parameters.getPbkdf2Iterations(),
parameters.getPbkdf2KeyLength()
);
return new HashResult(algorithm, hash256.toStorageFormat());
case PBKDF2_SHA512:
PBKDF2Service.PBKDF2Hash hash512 = pbkdf2Service.hashPassword(
password,
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA512,
parameters.getPbkdf2Iterations(),
parameters.getPbkdf2KeyLength()
);
return new HashResult(algorithm, hash512.toStorageFormat());
default:
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
}
}
/**
* Verify password against stored hash (auto-detects algorithm)
*/
public boolean verifyPassword(String password, String storedHash) {
HashAlgorithm algorithm = detectAlgorithm(storedHash);
switch (algorithm) {
case BCRYPT:
return bcryptService.verifyPassword(password, storedHash);
case PBKDF2_SHA256:
case PBKDF2_SHA512:
return pbkdf2Service.verifyPassword(password, storedHash);
default:
return false;
}
}
/**
* Async verification for high load scenarios
*/
public CompletableFuture<Boolean> verifyPasswordAsync(String password, String storedHash) {
return CompletableFuture.supplyAsync(() -> verifyPassword(password, storedHash));
}
/**
* Verify with timing attack protection
*/
public boolean verifyPasswordSecure(String password, String storedHash) {
// Always compute hash even if format is invalid (constant time)
String dummyHash = getDummyHash(detectAlgorithm(storedHash));
try {
boolean result = verifyPassword(password, storedHash);
// Mask timing by always doing the same work
verifyPassword("dummy", dummyHash);
return result;
} catch (Exception e) {
verifyPassword("dummy", dummyHash);
return false;
}
}
/**
* Check if hash needs upgrading
*/
public boolean needsUpgrade(String storedHash, SecurityPolicy policy) {
HashAlgorithm algorithm = detectAlgorithm(storedHash);
switch (algorithm) {
case BCRYPT:
BcryptService.BcryptParams bcryptParams = bcryptService.extractParams(storedHash);
return bcryptParams.getCost() < policy.getMinBcryptCost();
case PBKDF2_SHA256:
case PBKDF2_SHA512:
PBKDF2Service.PBKDF2Hash pbkdf2Hash = PBKDF2Service.PBKDF2Hash.fromString(storedHash);
return pbkdf2Hash.getIterations() < policy.getMinPbkdf2Iterations() ||
pbkdf2Hash.getHash().length * 8 < policy.getMinKeyLength();
default:
return true;
}
}
/**
* Upgrade hash to stronger parameters
*/
public String upgradeHash(String password, String oldHash, SecurityPolicy policy) {
if (!needsUpgrade(oldHash, policy)) {
return oldHash;
}
HashAlgorithm algorithm = policy.getPreferredAlgorithm();
HashParameters params = HashParameters.fromPolicy(policy);
return hashPassword(password, algorithm, params).getHash();
}
/**
* Benchmark both algorithms for comparison
*/
public ComparisonResult benchmarkAlgorithms(String password, int targetTimeMs) {
ComparisonResult result = new ComparisonResult();
// Find BCrypt cost for target time
for (int cost = 4; cost <= 12; cost++) {
long start = System.nanoTime();
bcryptService.hashPassword(password, cost);
long duration = System.nanoTime() - start;
long durationMs = TimeUnit.NANOSECONDS.toMillis(duration);
result.addBcryptResult(cost, durationMs);
if (durationMs >= targetTimeMs) {
result.setRecommendedBcryptCost(cost);
break;
}
}
// Find PBKDF2 iterations for target time
for (int iterations = 10000; iterations <= 1000000; iterations *= 2) {
long start = System.nanoTime();
pbkdf2Service.hashPassword(
password,
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256,
iterations,
256
);
long duration = System.nanoTime() - start;
long durationMs = TimeUnit.NANOSECONDS.toMillis(duration);
result.addPbkdf2Result(iterations, durationMs);
if (durationMs >= targetTimeMs) {
result.setRecommendedPbkdf2Iterations(iterations);
break;
}
}
return result;
}
private HashAlgorithm detectAlgorithm(String hash) {
if (hash.startsWith("$2a$") || hash.startsWith("$2b$") || hash.startsWith("$2y$")) {
return HashAlgorithm.BCRYPT;
} else if (hash.startsWith("PBKDF2_WITH_HMAC_SHA256:")) {
return HashAlgorithm.PBKDF2_SHA256;
} else if (hash.startsWith("PBKDF2_WITH_HMAC_SHA512:")) {
return HashAlgorithm.PBKDF2_SHA512;
} else {
throw new IllegalArgumentException("Unknown hash algorithm");
}
}
private String getDummyHash(HashAlgorithm algorithm) {
switch (algorithm) {
case BCRYPT:
return "$2a$10$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12";
case PBKDF2_SHA256:
return "PBKDF2_WITH_HMAC_SHA256:10000:saltsaltsalt:hashhashhash";
case PBKDF2_SHA512:
return "PBKDF2_WITH_HMAC_SHA512:10000:saltsaltsalt:hashhashhash";
default:
return "";
}
}
// Data classes
public static class HashResult {
private final HashAlgorithm algorithm;
private final String hash;
private final long timestamp;
public HashResult(HashAlgorithm algorithm, String hash) {
this.algorithm = algorithm;
this.hash = hash;
this.timestamp = System.currentTimeMillis();
}
public HashAlgorithm getAlgorithm() { return algorithm; }
public String getHash() { return hash; }
public long getTimestamp() { return timestamp; }
}
public static class HashParameters {
private final int bcryptCost;
private final int pbkdf2Iterations;
private final int pbkdf2KeyLength;
private HashParameters(int bcryptCost, int pbkdf2Iterations, int pbkdf2KeyLength) {
this.bcryptCost = bcryptCost;
this.pbkdf2Iterations = pbkdf2Iterations;
this.pbkdf2KeyLength = pbkdf2KeyLength;
}
public static HashParameters fromPolicy(SecurityPolicy policy) {
return new HashParameters(
policy.getMinBcryptCost(),
policy.getMinPbkdf2Iterations(),
policy.getMinKeyLength()
);
}
public int getBcryptCost() { return bcryptCost; }
public int getPbkdf2Iterations() { return pbkdf2Iterations; }
public int getPbkdf2KeyLength() { return pbkdf2KeyLength; }
}
public static class SecurityPolicy {
private final HashAlgorithm preferredAlgorithm;
private final int minBcryptCost;
private final int minPbkdf2Iterations;
private final int minKeyLength;
private final int targetTimeMs;
private SecurityPolicy(Builder builder) {
this.preferredAlgorithm = builder.preferredAlgorithm;
this.minBcryptCost = builder.minBcryptCost;
this.minPbkdf2Iterations = builder.minPbkdf2Iterations;
this.minKeyLength = builder.minKeyLength;
this.targetTimeMs = builder.targetTimeMs;
}
public HashAlgorithm getPreferredAlgorithm() { return preferredAlgorithm; }
public int getMinBcryptCost() { return minBcryptCost; }
public int getMinPbkdf2Iterations() { return minPbkdf2Iterations; }
public int getMinKeyLength() { return minKeyLength; }
public int getTargetTimeMs() { return targetTimeMs; }
public static class Builder {
private HashAlgorithm preferredAlgorithm = HashAlgorithm.BCRYPT;
private int minBcryptCost = 10;
private int minPbkdf2Iterations = 100000;
private int minKeyLength = 256;
private int targetTimeMs = 500;
public Builder preferredAlgorithm(HashAlgorithm algorithm) {
this.preferredAlgorithm = algorithm;
return this;
}
public Builder minBcryptCost(int cost) {
this.minBcryptCost = cost;
return this;
}
public Builder minPbkdf2Iterations(int iterations) {
this.minPbkdf2Iterations = iterations;
return this;
}
public Builder minKeyLength(int bits) {
this.minKeyLength = bits;
return this;
}
public Builder targetTimeMs(int timeMs) {
this.targetTimeMs = timeMs;
return this;
}
public SecurityPolicy build() {
return new SecurityPolicy(this);
}
}
}
public static class ComparisonResult {
private final Map<Integer, Long> bcryptResults = new HashMap<>();
private final Map<Integer, Long> pbkdf2Results = new HashMap<>();
private int recommendedBcryptCost;
private int recommendedPbkdf2Iterations;
public void addBcryptResult(int cost, long timeMs) {
bcryptResults.put(cost, timeMs);
}
public void addPbkdf2Result(int iterations, long timeMs) {
pbkdf2Results.put(iterations, timeMs);
}
public void setRecommendedBcryptCost(int cost) {
this.recommendedBcryptCost = cost;
}
public void setRecommendedPbkdf2Iterations(int iterations) {
this.recommendedPbkdf2Iterations = iterations;
}
public Map<Integer, Long> getBcryptResults() { return bcryptResults; }
public Map<Integer, Long> getPbkdf2Results() { return pbkdf2Results; }
public int getRecommendedBcryptCost() { return recommendedBcryptCost; }
public int getRecommendedPbkdf2Iterations() { return recommendedPbkdf2Iterations; }
public void printSummary() {
System.out.println("\n=== Performance Comparison ===");
System.out.println("BCrypt:");
bcryptResults.forEach((cost, time) ->
System.out.printf(" cost %2d: %d ms%n", cost, time));
System.out.println("Recommended cost: " + recommendedBcryptCost);
System.out.println("\nPBKDF2:");
pbkdf2Results.forEach((iter, time) ->
System.out.printf(" %,7d iterations: %d ms%n", iter, time));
System.out.println("Recommended iterations: " + recommendedPbkdf2Iterations);
}
}
}
5. Password Hashing REST API
package com.password.rest;
import com.password.hashing.HybridPasswordHashingService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/password")
public class PasswordHashingController {
private final HybridPasswordHashingService hashingService;
public PasswordHashingController(HybridPasswordHashingService hashingService) {
this.hashingService = hashingService;
}
@PostMapping("/hash/bcrypt")
public ResponseEntity<HashResponse> hashWithBcrypt(
@RequestBody HashRequest request) {
try {
HybridPasswordHashingService.HashResult result = hashingService.hashPassword(
request.getPassword(),
HybridPasswordHashingService.HashAlgorithm.BCRYPT,
HybridPasswordHashingService.HashParameters.fromPolicy(
createDefaultPolicy()
)
);
return ResponseEntity.ok(new HashResponse(
result.getHash(),
"bcrypt",
result.getTimestamp()
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(new HashResponse("Hashing failed: " + e.getMessage()));
}
}
@PostMapping("/hash/pbkdf2")
public ResponseEntity<HashResponse> hashWithPbkdf2(
@RequestBody HashRequest request,
@RequestParam(defaultValue = "SHA256") String algorithm) {
try {
HybridPasswordHashingService.HashAlgorithm algo =
"SHA512".equalsIgnoreCase(algorithm)
? HybridPasswordHashingService.HashAlgorithm.PBKDF2_SHA512
: HybridPasswordHashingService.HashAlgorithm.PBKDF2_SHA256;
HybridPasswordHashingService.HashResult result = hashingService.hashPassword(
request.getPassword(),
algo,
HybridPasswordHashingService.HashParameters.fromPolicy(
createDefaultPolicy()
)
);
return ResponseEntity.ok(new HashResponse(
result.getHash(),
"pbkdf2-" + algorithm,
result.getTimestamp()
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(new HashResponse("Hashing failed: " + e.getMessage()));
}
}
@PostMapping("/verify")
public ResponseEntity<VerifyResponse> verifyPassword(
@RequestBody VerifyRequest request) {
boolean isValid = hashingService.verifyPassword(
request.getPassword(),
request.getHash()
);
return ResponseEntity.ok(new VerifyResponse(
isValid,
isValid ? "Password valid" : "Invalid password"
));
}
@PostMapping("/verify/secure")
public ResponseEntity<VerifyResponse> verifyPasswordSecure(
@RequestBody VerifyRequest request) {
boolean isValid = hashingService.verifyPasswordSecure(
request.getPassword(),
request.getHash()
);
return ResponseEntity.ok(new VerifyResponse(
isValid,
isValid ? "Password valid" : "Invalid password"
));
}
@PostMapping("/needs-upgrade")
public ResponseEntity<UpgradeCheckResponse> checkUpgrade(
@RequestBody UpgradeCheckRequest request) {
HybridPasswordHashingService.SecurityPolicy policy =
new HybridPasswordHashingService.SecurityPolicy.Builder()
.minBcryptCost(request.getMinBcryptCost())
.minPbkdf2Iterations(request.getMinPbkdf2Iterations())
.minKeyLength(request.getMinKeyLength())
.build();
boolean needsUpgrade = hashingService.needsUpgrade(
request.getHash(),
policy
);
return ResponseEntity.ok(new UpgradeCheckResponse(
needsUpgrade,
needsUpgrade ? "Hash needs upgrade" : "Hash meets requirements"
));
}
@PostMapping("/upgrade")
public ResponseEntity<HashResponse> upgradeHash(
@RequestBody UpgradeRequest request) {
try {
HybridPasswordHashingService.SecurityPolicy policy =
new HybridPasswordHashingService.SecurityPolicy.Builder()
.preferredAlgorithm(
HybridPasswordHashingService.HashAlgorithm.valueOf(
request.getTargetAlgorithm()
)
)
.minBcryptCost(request.getTargetBcryptCost())
.minPbkdf2Iterations(request.getTargetPbkdf2Iterations())
.minKeyLength(request.getTargetKeyLength())
.build();
String upgradedHash = hashingService.upgradeHash(
request.getPassword(),
request.getOldHash(),
policy
);
return ResponseEntity.ok(new HashResponse(
upgradedHash,
request.getTargetAlgorithm(),
System.currentTimeMillis()
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(new HashResponse("Upgrade failed: " + e.getMessage()));
}
}
@GetMapping("/benchmark")
public ResponseEntity<BenchmarkResponse> benchmark(
@RequestParam String password,
@RequestParam(defaultValue = "500") int targetTimeMs) {
HybridPasswordHashingService.ComparisonResult result =
hashingService.benchmarkAlgorithms(password, targetTimeMs);
return ResponseEntity.ok(new BenchmarkResponse(
result.getBcryptResults(),
result.getPbkdf2Results(),
result.getRecommendedBcryptCost(),
result.getRecommendedPbkdf2Iterations()
));
}
private HybridPasswordHashingService.SecurityPolicy createDefaultPolicy() {
return new HybridPasswordHashingService.SecurityPolicy.Builder()
.preferredAlgorithm(HybridPasswordHashingService.HashAlgorithm.BCRYPT)
.minBcryptCost(12)
.minPbkdf2Iterations(310000) // OWASP recommended for SHA256
.minKeyLength(256)
.targetTimeMs(500)
.build();
}
// Request/Response classes
public static class HashRequest {
private String password;
// getter and setter
}
public static class HashResponse {
private String hash;
private String algorithm;
private long timestamp;
private String error;
public HashResponse(String hash, String algorithm, long timestamp) {
this.hash = hash;
this.algorithm = algorithm;
this.timestamp = timestamp;
}
public HashResponse(String error) {
this.error = error;
}
// getters
}
public static class VerifyRequest {
private String password;
private String hash;
// getters and setters
}
public static class VerifyResponse {
private boolean valid;
private String message;
public VerifyResponse(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
// getters
}
public static class UpgradeCheckRequest {
private String hash;
private int minBcryptCost;
private int minPbkdf2Iterations;
private int minKeyLength;
// getters and setters
}
public static class UpgradeCheckResponse {
private boolean needsUpgrade;
private String message;
public UpgradeCheckResponse(boolean needsUpgrade, String message) {
this.needsUpgrade = needsUpgrade;
this.message = message;
}
// getters
}
public static class UpgradeRequest {
private String password;
private String oldHash;
private String targetAlgorithm;
private int targetBcryptCost;
private int targetPbkdf2Iterations;
private int targetKeyLength;
// getters and setters
}
public static class BenchmarkResponse {
private Map<Integer, Long> bcryptResults;
private Map<Integer, Long> pbkdf2Results;
private int recommendedBcryptCost;
private int recommendedPbkdf2Iterations;
public BenchmarkResponse(Map<Integer, Long> bcryptResults,
Map<Integer, Long> pbkdf2Results,
int recommendedBcryptCost,
int recommendedPbkdf2Iterations) {
this.bcryptResults = bcryptResults;
this.pbkdf2Results = pbkdf2Results;
this.recommendedBcryptCost = recommendedBcryptCost;
this.recommendedPbkdf2Iterations = recommendedPbkdf2Iterations;
}
// getters
}
}
6. Testing and Comparison
package com.password.test;
import com.password.hashing.bcrypt.BcryptService;
import com.password.hashing.pbkdf2.PBKDF2Service;
import org.junit.jupiter.api.*;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PasswordHashingTest {
private BcryptService bcryptService;
private PBKDF2Service pbkdf2Service;
@BeforeEach
void setUp() {
bcryptService = new BcryptService();
pbkdf2Service = new PBKDF2Service();
}
@Test
@Order(1)
void testBcryptHashAndVerify() {
String password = "MySecurePassword123!";
String hash = bcryptService.hashPassword(password, 10);
assertNotNull(hash);
assertTrue(hash.startsWith("$2a$10$"));
assertEquals(60, hash.length());
boolean verified = bcryptService.verifyPassword(password, hash);
assertTrue(verified);
boolean wrongVerified = bcryptService.verifyPassword("WrongPassword", hash);
assertFalse(wrongVerified);
}
@Test
@Order(2)
void testBcryptDifferentCosts() {
String password = "testPassword";
for (int cost : new int[]{4, 8, 12}) {
long start = System.nanoTime();
String hash = bcryptService.hashPassword(password, cost);
long duration = System.nanoTime() - start;
long durationMs = TimeUnit.NANOSECONDS.toMillis(duration);
System.out.printf("BCrypt cost %2d: %d ms%n", cost, durationMs);
assertTrue(bcryptService.verifyPassword(password, hash));
}
}
@Test
@Order(3)
void testBcryptExtractParams() {
String hash = bcryptService.hashPassword("password", 12);
BcryptService.BcryptParams params = bcryptService.extractParams(hash);
assertEquals(12, params.getCost());
assertNotNull(params.getSalt());
}
@Test
@Order(4)
void testPBKDF2HashAndVerify() {
String password = "MySecurePassword123!";
PBKDF2Service.PBKDF2Hash hash = pbkdf2Service.hashPassword(
password,
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256,
100000,
256
);
assertNotNull(hash);
assertEquals(32, hash.getHash().length); // 256 bits = 32 bytes
assertEquals(16, hash.getSalt().length);
boolean verified = pbkdf2Service.verifyPassword(password, hash);
assertTrue(verified);
boolean wrongVerified = pbkdf2Service.verifyPassword("WrongPassword", hash);
assertFalse(wrongVerified);
}
@Test
@Order(5)
void testPBKDF2DifferentIterations() {
String password = "testPassword";
PBKDF2Service.PBKDF2Algorithm alg =
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256;
for (int iterations : new int[]{10000, 50000, 100000}) {
long start = System.nanoTime();
PBKDF2Service.PBKDF2Hash hash = pbkdf2Service.hashPassword(
password, alg, iterations, 256
);
long duration = System.nanoTime() - start;
long durationMs = TimeUnit.NANOSECONDS.toMillis(duration);
System.out.printf("PBKDF2 %,7d iterations: %d ms%n", iterations, durationMs);
assertTrue(pbkdf2Service.verifyPassword(password, hash));
}
}
@Test
@Order(6)
void testPBKDF2FormattedString() {
String password = "testPassword";
PBKDF2Service.PBKDF2Hash hash = pbkdf2Service.hashPassword(
password,
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256,
100000,
256
);
String formatted = hash.toStorageFormat();
PBKDF2Service.PBKDF2Hash parsed = PBKDF2Service.PBKDF2Hash.fromString(formatted);
assertArrayEquals(hash.getHash(), parsed.getHash());
assertArrayEquals(hash.getSalt(), parsed.getSalt());
assertEquals(hash.getIterations(), parsed.getIterations());
assertEquals(hash.getAlgorithm(), parsed.getAlgorithm());
}
@Test
@Order(7)
void testBcryptNeedsRehash() {
String hash = bcryptService.hashPassword("password", 8);
assertTrue(bcryptService.needsRehash(hash, 10));
assertFalse(bcryptService.needsRehash(hash, 8));
String upgradedHash = bcryptService.migrateHash("password", hash, 10);
assertFalse(bcryptService.needsRehash(upgradedHash, 10));
}
@Test
@Order(8)
void testPBKDF2NeedsRehash() {
PBKDF2Service.PBKDF2Hash hash = pbkdf2Service.hashPassword(
"password",
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256,
50000,
256
);
assertTrue(pbkdf2Service.needsRehash(hash, 100000, 256));
assertFalse(pbkdf2Service.needsRehash(hash, 50000, 256));
PBKDF2Service.PBKDF2Hash upgraded = pbkdf2Service.migrateHash(
"password", hash, 100000, 256
);
assertFalse(pbkdf2Service.needsRehash(upgraded, 100000, 256));
}
@Test
@Order(9)
void testFindOptimalIterations() {
String password = "testPassword";
PBKDF2Service.PBKDF2Algorithm alg =
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256;
int iterations = pbkdf2Service.findOptimalIterations(password, alg, 500);
System.out.println("Optimal iterations for 500ms: " + iterations);
long start = System.nanoTime();
pbkdf2Service.hashPassword(password, alg, iterations, 256);
long duration = System.nanoTime() - start;
long durationMs = TimeUnit.NANOSECONDS.toMillis(duration);
assertTrue(durationMs >= 400 && durationMs <= 600,
"Duration: " + durationMs + "ms");
}
@Test
@Order(10)
void testConstantTimeComparison() {
String password = "password";
String hash = bcryptService.hashPassword(password, 10);
// Multiple verifications should take similar time
long[] times = new long[5];
for (int i = 0; i < 5; i++) {
long start = System.nanoTime();
bcryptService.verifyPasswordConstantTime(password, hash);
times[i] = System.nanoTime() - start;
}
// Check for timing consistency
long avg = (times[0] + times[1] + times[2] + times[3] + times[4]) / 5;
for (long time : times) {
long deviation = Math.abs(time - avg);
assertTrue(deviation < avg / 2, "Timing deviation too high: " + deviation);
}
}
@Test
@Order(11)
void testPerformanceComparison() {
String password = "testPassword";
int iterations = 10;
// BCrypt with cost 10
long bcryptTotal = 0;
for (int i = 0; i < iterations; i++) {
long start = System.nanoTime();
bcryptService.hashPassword(password, 10);
bcryptTotal += System.nanoTime() - start;
}
long bcryptAvg = bcryptTotal / iterations;
// PBKDF2 with 100,000 iterations
long pbkdf2Total = 0;
for (int i = 0; i < iterations; i++) {
long start = System.nanoTime();
pbkdf2Service.hashPassword(
password,
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256,
100000,
256
);
pbkdf2Total += System.nanoTime() - start;
}
long pbkdf2Avg = pbkdf2Total / iterations;
System.out.println("\n=== Performance Comparison ===");
System.out.printf("BCrypt (cost=10): %,d ns (%,d ms)%n",
bcryptAvg, TimeUnit.NANOSECONDS.toMillis(bcryptAvg));
System.out.printf("PBKDF2 (100k): %,d ns (%,d ms)%n",
pbkdf2Avg, TimeUnit.NANOSECONDS.toMillis(pbkdf2Avg));
// Both should be within reasonable range
assertTrue(bcryptAvg > 10_000_000, "BCrypt too fast");
assertTrue(pbkdf2Avg > 10_000_000, "PBKDF2 too fast");
}
@Test
@Order(12)
void testHashUpgrade() {
// Start with weaker hash
String password = "MyPassword123";
String oldHash = bcryptService.hashPassword(password, 8);
// Upgrade to stronger
String upgradedHash = bcryptService.migrateHash(password, oldHash, 12);
assertNotEquals(oldHash, upgradedHash);
assertTrue(upgradedHash.startsWith("$2a$12$"));
assertTrue(bcryptService.verifyPassword(password, upgradedHash));
}
@Test
@Order(13)
void testInvalidInputs() {
// Test with null
assertFalse(bcryptService.verifyPassword(null, "$2a$10$..."));
assertFalse(bcryptService.verifyPassword("password", null));
// Test with invalid hash format
assertFalse(bcryptService.verifyPassword("password", "invalid"));
// Test with wrong version
String hash = bcryptService.hashPassword("password", 10);
String tampered = hash.replace("$2a", "$2x");
assertFalse(bcryptService.verifyPassword("password", tampered));
}
@Test
@Order(14)
void testSaltUniqueness() {
String password = "samePassword";
java.util.Set<String> salts = new java.util.HashSet<>();
for (int i = 0; i < 100; i++) {
BcryptService.BcryptParams params = bcryptService.extractParams(
bcryptService.hashPassword(password, 10)
);
salts.add(new String(params.getSalt()));
}
// Should have many unique salts
assertTrue(salts.size() > 90, "Salt uniqueness too low: " + salts.size());
}
@Test
@Order(15)
void testMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
// BCrypt uses more memory (4KB internal state)
runtime.gc();
long beforeBcrypt = runtime.totalMemory() - runtime.freeMemory();
bcryptService.hashPassword("password", 10);
runtime.gc();
long afterBcrypt = runtime.totalMemory() - runtime.freeMemory();
long bcryptMemory = afterBcrypt - beforeBcrypt;
// PBKDF2 uses less memory
runtime.gc();
long beforePbkdf2 = runtime.totalMemory() - runtime.freeMemory();
pbkdf2Service.hashPassword(
"password",
PBKDF2Service.PBKDF2Algorithm.PBKDF2_WITH_HMAC_SHA256,
100000,
256
);
runtime.gc();
long afterPbkdf2 = runtime.totalMemory() - runtime.freeMemory();
long pbkdf2Memory = afterPbkdf2 - beforePbkdf2;
System.out.println("\n=== Memory Usage ===");
System.out.printf("BCrypt memory: ~%d bytes%n", bcryptMemory);
System.out.printf("PBKDF2 memory: ~%d bytes%n", pbkdf2Memory);
// BCrypt typically uses more memory
assertTrue(bcryptMemory > pbkdf2Memory);
}
}
7. Spring Security Integration
package com.password.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.*;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// Create delegating password encoder for multiple formats
Map<String, PasswordEncoder> encoders = new HashMap<>();
// BCrypt encoders
encoders.put("bcrypt", new BCryptPasswordEncoder(12));
encoders.put("2a", new BCryptPasswordEncoder(12));
encoders.put("2b", new BCryptPasswordEncoder(12));
// PBKDF2 encoders
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
// SCrypt (memory-hard alternative)
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
// Legacy MD5 (for migration only!)
encoders.put("md5", new MessageDigestPasswordEncoder("MD5"));
// NoOp for plain text (development only!)
encoders.put("noop", NoOpPasswordEncoder.getInstance());
DelegatingPasswordEncoder delegatingEncoder =
new DelegatingPasswordEncoder("bcrypt", encoders);
// Set default for unencoded passwords
delegatingEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder(12));
return delegatingEncoder;
}
@Bean
public BCryptPasswordEncoder bcryptEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public Pbkdf2PasswordEncoder pbkdf2Encoder() {
return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
}
// Custom password encoder with monitoring
@Bean
public PasswordEncoder monitoredPasswordEncoder() {
return new MonitoredPasswordEncoder();
}
// Custom encoder that monitors performance
public static class MonitoredPasswordEncoder implements PasswordEncoder {
private final PasswordEncoder delegate = new BCryptPasswordEncoder(12);
private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
@Override
public String encode(CharSequence rawPassword) {
long start = System.nanoTime();
String encoded = delegate.encode(rawPassword);
long duration = System.nanoTime() - start;
log.info("Password encoding took {} ms",
TimeUnit.NANOSECONDS.toMillis(duration));
return encoded;
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
long start = System.nanoTime();
boolean matches = delegate.matches(rawPassword, encodedPassword);
long duration = System.nanoTime() - start;
log.debug("Password verification took {} ms",
TimeUnit.NANOSECONDS.toMillis(duration));
return matches;
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
return delegate.upgradeEncoding(encodedPassword);
}
}
}
Security Recommendations
1. Current Best Practices (2024)
| Parameter | Bcrypt | PBKDF2 |
|---|---|---|
| Minimum Cost/Iterations | Cost 10-12 | 310,000 iterations (SHA256) |
| Recommended | Cost 12-14 | 600,000+ iterations |
| High Security | Cost 14-16 | 1,000,000+ iterations |
| Key Length | 184 bits (embedded) | 256-512 bits |
| Salt Length | 128 bits (embedded) | 128-256 bits |
2. Algorithm Selection Guide
public class AlgorithmSelector {
public PasswordEncoder selectEncoder(Requirements requirements) {
if (requirements.isFipsCompliant()) {
// PBKDF2 is FIPS 140-2 compliant
return new Pbkdf2PasswordEncoder("", 310000, 256);
}
if (requirements.isHighSecurity()) {
// Bcrypt with high cost
return new BCryptPasswordEncoder(14);
}
if (requirements.isLegacyCompatible()) {
// Bcrypt (most compatible)
return new BCryptPasswordEncoder(12);
}
// Default to bcrypt
return new BCryptPasswordEncoder(12);
}
public static class Requirements {
private boolean fipsCompliant;
private boolean highSecurity;
private boolean legacyCompatible;
// getters and setters
}
}
3. Migration Strategy
public class PasswordMigrationService {
private final PasswordEncoder oldEncoder;
private final PasswordEncoder newEncoder;
private final UserRepository userRepository;
public void migrateUserPassword(String username, String rawPassword) {
User user = userRepository.findByUsername(username);
// Check if password needs rehashing
if (PasswordEncodingUtils.detectEncoding(user.getPassword()) != Encoding.BCRYPT) {
String newHash = newEncoder.encode(rawPassword);
user.setPassword(newHash);
userRepository.save(user);
}
}
public boolean verifyAndMigrate(String username, String rawPassword) {
User user = userRepository.findByUsername(username);
if (oldEncoder.matches(rawPassword, user.getPassword())) {
// Password valid with old encoder - upgrade it
String newHash = newEncoder.encode(rawPassword);
user.setPassword(newHash);
userRepository.save(user);
return true;
}
return newEncoder.matches(rawPassword, user.getPassword());
}
}
Conclusion
Bcrypt Advantages
- Built-in salt storage in hash string
- Memory-hard (4KB internal state)
- Limited parallelization on GPUs
- Simple to use with self-contained hashes
- Industry standard for web applications
PBKDF2 Advantages
- FIPS 140-2 compliant for government use
- Configurable output length (up to 512 bits)
- HMAC-based with proven security
- Hardware accelerated on some platforms
- Standardized (RFC 2898, NIST SP 800-132)
When to Use Each
Choose Bcrypt when:
- Building web applications
- Need simple deployment (salt in hash)
- Want GPU/ASIC resistance
- Need PHC (Password Hashing Competition) compliance
Choose PBKDF2 when:
- Requiring FIPS 140-2 compliance
- Needing longer output keys (e.g., for encryption)
- Working with legacy systems
- Requiring NIST certification
For maximum security, consider Argon2id (the PHC winner) or a hybrid approach that combines both algorithms.
Advanced Java Programming Concepts and Projects (Related to Java Programming)
Number Guessing Game in Java:
This project teaches how to build a simple number guessing game using Java. It combines random number generation, loops, and conditional statements to create an interactive program where users guess a number until they find the correct answer.
Read more: https://macronepal.com/blog/number-guessing-game-in-java-a-complete-guide/
HashMap Basics in Java:
HashMap is a collection class used to store data in key-value pairs. It allows fast retrieval of values using keys and is widely used when working with structured data that requires quick searching and updating.
Read more: https://macronepal.com/blog/hashmap-basics-in-java-a-complete-guide/
Date and Time in Java:
This topic explains how to work with dates and times in Java using built-in classes. It helps developers manage time-related data such as current date, formatting time, and calculating time differences.
Read more: https://macronepal.com/blog/date-and-time-in-java-a-complete-guide/
StringBuilder in Java:
StringBuilder is used to create and modify strings efficiently. Unlike regular strings, it allows changes without creating new objects, making programs faster when handling large or frequently changing text.
Read more: https://macronepal.com/blog/stringbuilder-in-java-a-complete-guide/
Packages in Java:
Packages help organize Java classes into groups, making programs easier to manage and maintain. They also help prevent naming conflicts and improve code structure in large applications.
Read more: https://macronepal.com/blog/packages-in-java-a-complete-guide/
Interfaces in Java:
Interfaces define a set of methods that classes must implement. They help achieve abstraction and support multiple inheritance in Java, making programs more flexible and organized.
Read more: https://macronepal.com/blog/interfaces-in-java-a-complete-guide/
Abstract Classes in Java:
Abstract classes are classes that cannot be instantiated directly and may contain both abstract and non-abstract methods. They are used as base classes to define common features for other classes.
Read more: https://macronepal.com/blog/abstract-classes-in-java-a-complete-guide/
Method Overriding in Java:
Method overriding occurs when a subclass provides its own version of a method already defined in its parent class. It supports runtime polymorphism and allows customized behavior in child classes.
Read more: https://macronepal.com/blog/method-overriding-in-java-a-complete-guide/
The This Keyword in Java:
The this keyword refers to the current object in a class. It is used to access instance variables, call constructors, and differentiate between class variables and parameters.
Read more: https://macronepal.com/blog/the-this-keyword-in-java-a-complete-guide/
Encapsulation in Java:
Encapsulation is an object-oriented concept that involves bundling data and methods into a single unit and restricting direct access to some components. It improves data security and program organization.
Read more: https://macronepal.com/blog/encapsulation-in-java-a-complete-guide/