Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
187: Secure Password Hashing in Java
I've seen a lot of junior developers make the same mistake when they first build a login system: they think "hashing" is a binary state. You're either storing passwords in plain text (which is a fireable offense in most shops) or you're "hashing them," and therefore you're secure. But as you'll see today, not all hashes are created equal.
The temptation of MessageDigest
When I first started, I thought the built-in java.security.MessageDigest class was the way to go. It's right there in the JDK, it's fast, and it supports SHA-256. Let's look at how I used to write a simple hashing method:
public String hashPassword(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encodedhash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
At first glance, this looks fine. If you enter "Password123", you get a consistent, garbled string. If the database leaks, the attacker doesn't see "Password123" immediately. But there's a massive problem here that I didn't realize at the time.
Why I just shot myself in the foot
Here is the thing about SHA-256: it is too fast. It was designed to verify large files quickly, not to protect passwords. A modern GPU can calculate billions of SHA-256 hashes per second. If an attacker gets my database, they can use a "Rainbow Table"—a precomputed list of millions of common passwords and their SHA-256 hashes—to crack most of my users' passwords in milliseconds.
Even worse, my code above has no "salt." If two users have the same password, they have the same hash. If an attacker cracks one, they've cracked both. I realized I needed an algorithm that was intentionally slow and handled salting automatically.
Switching to BCrypt for real security
In the professional world, we almost never write our own hashing logic using MessageDigest. Instead, we use BCrypt. BCrypt is a "slow" hash. It includes a "work factor" (cost) that determines how many iterations the algorithm runs. This makes brute-force attacks computationally expensive and practically impossible for the average attacker.
Since BCrypt isn't in the standard JDK, I'll use the jBcrypt library. It's the industry standard for a reason: it handles the salt generation and storage for you, embedding the salt directly into the resulting hash string.
import org.mindrot.jbcrypt.BCrypt;
public class PasswordService {
// A cost factor of 12 is a good balance between security and performance
private static final int WORK_FACTOR = 12;
public String hashPassword(String plainTextPassword) {
// gensalt() creates a random salt and incorporates the work factor
return BCrypt.hashpw(plainTextPassword, BCrypt.gensalt(WORK_FACTOR));
}
public boolean verifyPassword(String plainTextPassword, String hashedPassword) {
// checkpw extracts the salt from the hashedPassword and uses it to hash the input
return BCrypt.checkpw(plainTextPassword, hashedPassword);
}
}
Putting it all together in a User service
Now, let's see how this actually lives inside a service. Notice that I'm not storing the salt in a separate database column. BCrypt's hashpw method returns a string that contains the algorithm version, the cost factor, the salt, and the hash all in one. This simplifies our database schema significantly.
public class UserService {
private final PasswordService passwordService = new PasswordService();
private final Map<String, String> userDatabase = new HashMap<>(); // Simulating a DB
public void registerUser(String username, String password) {
String secureHash = passwordService.hashPassword(password);
userDatabase.put(username, secureHash);
System.out.println("User " + username + " registered successfully.");
}
public boolean login(String username, String password) {
String storedHash = userDatabase.get(username);
if (storedHash == null) return false;
return passwordService.verifyPassword(password, storedHash);
}
}
By shifting from a fast, unsalted hash to BCrypt, we've moved from a system that could be cracked in seconds to one that would take years of compute time to break. That is the difference between "it works" and "it's secure."
📋 Practical Task
Build a Password Complexity Guard for BCrypt
Using the PasswordService logic from the lesson, create a new class called SecureUserRegistration. Your task is to implement a registration method that ensures a password is not hashed unless it meets the following "complexity" requirements:
- Minimum of 10 characters.
- Must contain at least one digit.
- Must contain at least one uppercase letter.
If the password fails any of these checks, the method should throw a custom InvalidPasswordException and prevent the BCrypt.hashpw method from ever being called. This prevents the system from wasting CPU cycles hashing passwords that are too weak to be secure anyway.
There are no comments for now.