Skip to Content
Course content

187: Secure Password Hashing in Java

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.