Secure Password Storage

Introduction

Welcome to the lesson on secure password storage in our course! In this lesson, we will explore the critical role of key derivation functions (KDFs) in protecting passwords. Building on our previous discussions about cryptographic failures, we'll focus on how improper password storage can lead to vulnerabilities.

Let's dive in and learn how to safeguard passwords effectively! 🔐

Password Storage and Security Risks

Password storage is a crucial aspect of web application security. When users create accounts, their passwords must be stored securely to prevent unauthorized access. When passwords are stored without proper hashing techniques, several critical vulnerabilities emerge:

  1. Plain text storage: Storing passwords in plain text means anyone with database access can immediately see all user passwords.
  2. Simple hashing without salt: Using basic hash functions like MD5 or SHA-256 without salt creates predictable outputs for identical inputs.
  3. Fast hashing algorithms: Algorithms designed for speed (like SHA-256) allow attackers to attempt millions of password combinations per second.
  4. Lack of salt: Without unique salts, identical passwords produce identical hashes, enabling efficient attacks against multiple accounts simultaneously.

Key derivation functions (KDFs) are essential tools that transform passwords into secure hashes, making it difficult for attackers to reverse-engineer the original passwords.

Now that we understand the importance of proper password storage and the risks of inadequate protection, let's examine some concrete examples of vulnerable implementations.

Insecure Password Storage

Suppose you have passwords that you need to hash before storing them in your database. A common but insecure approach would be to store passwords in plain text or use direct string comparison for verification.

package com.codesignal.pastebin.controller;

import com.codesignal.pastebin.model.User;
import com.codesignal.pastebin.repo.UserRepository;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class InsecureAuthController {
    private final UserRepository users;

    public InsecureAuthController(UserRepository users) {
        this.users = users;
    }

    @PostMapping("/register")
    public void register(@RequestBody RegisterRequest request) {
        User user = new User();
        user.setUsername(request.username());
        user.setPassword(request.password()); // Weak: Storing plain text password
        users.save(user);
    }

    @PostMapping("/login")
    public boolean login(@RequestBody LoginRequest request) {
        User user = users.findByUsername(request.username()).orElse(null);
        // Weak: Direct password comparison
        return user != null && request.password().equals(user.getPassword());
    }

    public record RegisterRequest(String username, String password) {}
    public record LoginRequest(String username, String password) {}
}

This verification method highlights the vulnerability - passwords are stored in plain text and compared directly, making the system extremely insecure.

Let's explore how attackers can exploit these weaknesses.

Exploiting the Vulnerability

Without proper hashing and salting, attackers can employ several effective techniques:

  1. Database breach exposure: If attackers gain database access, they can immediately see all user passwords in plain text.
  2. Rainbow table attacks: Pre-computed tables allow attackers to instantly look up common password hashes. The hash for "password" would be immediately identified from these tables.
  3. Brute force attacks: Using specialized hardware, attackers can test billions of password combinations per second against simple hashes.
  4. Identical password detection: Without salts, identical passwords produce identical hashes. If two users have the same password, they'll have the same hash, allowing attackers to compromise multiple accounts once one password is cracked.

In a real-world breach of a database with unsalted or plain text passwords, an attacker could crack the most common passwords in minutes to hours, rather than the years it would take with proper KDFs. Fortunately, there are robust solutions available to prevent these attacks.

Secure Example: Password Storage with KDFs

To protect against these vulnerabilities, we should use proper key derivation functions like BCrypt, Argon2, or PBKDF2. These functions automatically incorporate salting and work factor adjustments to make password cracking computationally expensive.

Spring Security provides a PasswordEncoder interface with BCrypt implementation that is specifically designed for password hashing. It automatically handles salt generation and includes a work factor parameter that allows you to adjust the computational cost as hardware becomes more powerful.

First, configure the PasswordEncoder bean:

package com.codesignal.pastebin.config;

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.PasswordEncoder;

@Configuration
public class AppConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // Uses default work factor of 10
    }
}

Then use it in your authentication controller:

package com.codesignal.pastebin.controller;

import com.codesignal.pastebin.model.User;
import com.codesignal.pastebin.repo.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class SecureAuthController {
    private final UserRepository users;
    private final PasswordEncoder encoder;

    public SecureAuthController(UserRepository users, PasswordEncoder encoder) {
        this.users = users;
        this.encoder = encoder;
    }

    @PostMapping("/register")
    public void register(@RequestBody RegisterRequest request) {
        User user = new User();
        user.setUsername(request.username());
        user.setPassword(encoder.encode(request.password())); // Secure: Hash with BCrypt
        users.save(user);
    }

    @PostMapping("/login")
    public boolean login(@RequestBody LoginRequest request) {
        User user = users.findByUsername(request.username()).orElse(null);
        // Secure: Use BCrypt comparison with automatic salt handling
        return user != null && encoder.matches(request.password(), user.getPassword());
    }

    public record RegisterRequest(String username, String password) {}
    public record LoginRequest(String username, String password) {}
}

This implementation uses Spring Security's BCrypt PasswordEncoder with automatic salting and a default work factor of 10. The salt ensures that even identical passwords produce different hashes, while the work factor makes brute force attacks prohibitively expensive.

The encoder.matches() method securely compares the provided password with the stored hashed password without revealing any information that could help attackers.

Note that while KDFs significantly increase the security of password storage, they cannot protect against fundamentally weak passwords - proper password selection by users remains crucial for overall system security.

Conclusion and Next Steps

In this lesson, we've explored the critical importance of using KDFs for secure password storage. We've seen how the absence of proper salting and slow hashing algorithms can lead to catastrophic security breaches, allowing attackers to quickly compromise user accounts. By implementing solutions like jBCrypt (BCrypt algorithm), you can significantly enhance the security of your applications and protect sensitive user data from unauthorized access.

As you move on to the practice exercises, you'll have the opportunity to apply these concepts and enhance your skills in web application security. Keep up the great work, and continue exploring the fascinating world of cryptography! 🎉

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal