Authentication

Authentication is the process of verifying identity. The most common method is the password. But how do you store a password securely?

Rule #1 of Security: NEVER store passwords in plain text.

If you store passwords in a database like SELECT * FROM users, a single SQL Injection leak compromises everyone. Instead, we store a Hash.

The 2012 LinkedIn Breach (A War Story)

In 2012, hackers stole 6.5 million passwords from LinkedIn. Why was it so devastating? LinkedIn was storing passwords using SHA-1 without a salt. Hackers used pre-computed tables to instantly reverse millions of hashes back into plain text. This leads us to the mechanics of hashing and how to defend against these attacks.

1. The Physics of Hashing

A Hash Function (like SHA-256) is a one-way mathematical trapdoor. It’s like a meat grinder: you can easily turn a steak into ground beef, but you cannot turn ground beef back into a steak. Hash("password123")ef92b7...

The Attack: Rainbow Tables

Hackers know human nature. People use terrible passwords. Attackers pre-compute the hashes for billions of common passwords (like “123456” or “qwerty”) and store them in massive databases called Rainbow Tables. If your database leaks and contains ef92b7..., they look it up in their table and instantly know the password is “password123”.

The Defense: Salting

A Salt is a random string added to the password before hashing. Hash("password123" + "RandomSaltX")TotallyDifferentHash

This defeats pre-computed tables. Why? Because the attacker would need to generate a completely new multi-terabyte Rainbow Table for every unique salt in your database, which is computationally impossible.

The Defense: Peppering

A Pepper is a secret key added to the hash process, but crucially, it is stored outside the database (e.g., in a secure KMS/HSM or an environment variable). Hash("password123" + Salt + Pepper)

If a hacker steals your database via SQL Injection, they get the hashes and the salts, but they don’t have the Pepper. Without the Pepper, they cannot begin cracking the hashes offline.


2. Interactive: Salted Hash Visualizer

See how two users with the same password get different hashes due to unique salts. Try toggling the salts off to see what happens when users share a password in a poorly designed system.

User A (Alice)
Password
Salt (Random)
x8L2p9
Stored Hash
...
User B (Bob)
Password
Salt (Random)
m4K1sZ
Stored Hash
...
Even though passwords are identical, the unique salts ensure the hashes are completely different.

3. Work Factors: Why Speed is Bad

You might think: “I want a fast hash function!” Wrong. Modern GPUs can calculate billions of hashes per second. If your hash function is fast (like MD5 or SHA-256), an attacker with a rig of GPUs can brute-force a stolen hash in seconds.

You want a hash that is deliberately slow. A login that takes 100 milliseconds is imperceptible to a human user logging in once, but it completely destroys the economics of a brute-force attack attempting billions of guesses. This is the Work Factor (or Cost).

The Rise of Memory-Hard Algorithms

Hackers countered slow algorithms by building custom ASICs (specialized hardware chips) that do nothing but calculate hashes incredibly fast.

To defeat ASICs and GPUs, modern algorithms are Memory-Hard. They require a significant amount of RAM to compute a single hash. A GPU might have thousands of cores, but it shares a relatively small memory pool. By demanding memory, you bottleneck the hardware, rendering massive parallel brute-forcing useless.

  • Bad: MD5, SHA-1, SHA-256 (Too fast, easily accelerated by GPUs/ASICs).
  • Good: Argon2, Bcrypt, Scrypt (Memory-hard and CPU-hard, tunable work factors).

4. Anatomy of a Secure Login Request

When a user submits a password, here is the step-by-step reality of what happens in a secure system:

  1. Client TLS: The user types “password123”. It is encrypted over HTTPS (TLS) so ISPs and network sniffers cannot read it.
  2. Server Ingress: The backend application receives the plain text password in memory.
  3. Database Lookup: The backend queries the database for the user’s email. It retrieves the stored Hash and the unique Salt.
  4. Pepper Application: The backend retrieves the global Pepper from its secure environment variables.
  5. Compute & Compare: The backend computes Argon2(password123 + Salt + Pepper). It then compares this computed hash against the stored hash.
  6. Constant-Time Comparison: Crucially, the string comparison must be done in “constant time” (e.g., crypto.timingSafeEqual). If you use a normal string equals (==), it fails early on the first wrong character. Hackers can measure these nanosecond timing differences to guess the hash character by character!

5. Code Example: Secure Hashing

Never write your own cryptography. Use industry-standard libraries. Notice how bcrypt and argon2 automatically generate and embed the salt into the final resulting string for you.

Go
Java
package main

import (
	"fmt"
	"golang.org/x/crypto/bcrypt"
)

func main() {
	password := []byte("secret123")

	// Hashing the password.
	// Cost 10 means 2^10 iterations. This is the Work Factor.
    // bcrypt automatically generates a secure random salt for us.
	hash, err := bcrypt.GenerateFromPassword(password, 10)
	if err != nil {
		panic(err)
	}
    // The resulting 'hash' string contains the algorithm version, cost, salt, AND the actual hash.
	fmt.Printf("Stored Hash: %s\n", hash)

	// Verifying the password
    // CompareHashAndPassword securely extracts the salt from the stored 'hash',
    // hashes the incoming 'password', and performs a constant-time comparison.
	err = bcrypt.CompareHashAndPassword(hash, password)
	if err == nil {
		fmt.Println("Success: Password matches.")
	} else {
		fmt.Println("Failure: Access Denied.")
	}
}
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;

public class PasswordUtil {
    // PBKDF2 (Password-Based Key Derivation Function 2)
    // Standard in Java (Argon2 requires external libs like BouncyCastle)

    public static String hashPassword(String password, byte[] salt) throws Exception {
        // Work Factor: 65,536 iterations to slow down brute force
        int iterations = 65536;
        int keyLength = 256;

        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLength);
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] hash = factory.generateSecret(spec).getEncoded();

        return Base64.getEncoder().encodeToString(hash);
    }

    public static void main(String[] args) throws Exception {
        // Always generate a unique secure random salt per user
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);

        String hash = hashPassword("secret123", salt);
        System.out.println("Hash: " + hash);

        // In a real system, you must store BOTH the 'hash' AND the 'salt' in the DB row
    }
}