Security

A Beginner's Guide to Simple Password Hashing for Better Security

Elias Vance · · 18 min read

⚡ The short answer

Learn the basics of password hashing, why it's crucial for security, and how simple salting protects user data from common attacks.

Read the long version ↓
A Beginner's Guide to Simple Password Hashing for Better Security

Imagine you’ve just signed up for a new online service. You type in your brand-new, complex password, hit ‘submit,’ and feel a small sense of satisfaction. But have you ever stopped to think about what happens to that password once it leaves your computer? Most users assume it’s stored securely, perhaps encrypted, but few truly understand the underlying mechanism that keeps their credentials safe from hackers. The reality is, if your password is stored in plain text, it’s an accident waiting to happen. A data breach, even a small one, can expose every user’s clear-text password, leading to widespread account compromise across multiple services if they reuse passwords. This isn’t just a theoretical risk; it happens with alarming frequency. My experience in reviewing countless security incidents reveals that inadequate password storage is a primary vulnerability.

Key Takeaways

  • Always store user passwords as hashed values, not plain text, to prevent mass compromise during data breaches.
  • Implement a unique ‘salt’ for each password before hashing to defend against rainbow table attacks.
  • Choose modern, computationally intensive hashing algorithms like bcrypt or Argon2 over older, faster ones like MD5 or SHA-1.
  • Regularly review and update your hashing strategy as new security threats and recommendations emerge.

The Fundamental Flaw of Storing Plain-Text Passwords

The most basic, yet shockingly common, mistake I see in fledgling online services is storing passwords in plain text. This isn’t just bad practice; it’s a catastrophic security vulnerability waiting to be exploited. Picture this: a hacker gains access to your database. If your users’ passwords are saved exactly as they typed them, that hacker now possesses a treasure trove of clear-text credentials. They don’t need to ‘crack’ anything; they simply read the passwords directly. This is akin to leaving your house keys under the doormat and expecting a burglar not to find them.

In my early days, I once audited a small e-commerce site that had taken this exact approach. The developers, well-meaning but inexperienced, believed that simply securing the database server was sufficient. They argued, “If the server is locked down, no one can get to the passwords anyway.” This reasoning completely misses the point. The strongest server security can be bypassed, whether through a zero-day exploit, a compromised employee, or a simple misconfiguration. When I demonstrated how easily I could access a file containing hundreds of clear-text passwords after simulating a basic SQL injection, the look on their faces was priceless. The realization that every single customer’s account was vulnerable, not just on their site but potentially on every other site where those users reused passwords, was a wake-up call. A single breach of their system could cascade into dozens or hundreds of compromises elsewhere.

What changed everything for me was understanding that defense-in-depth applies heavily to data storage. You assume a breach will happen. Therefore, you must store data in a way that minimizes damage when it does. For passwords, this means making them unreadable even if the database is exposed. Plain-text storage ensures maximum damage, transforming a localized breach into a potential widespread identity theft event for your users. Never, ever store passwords as plain text. It’s the first commandment of password security.

Why Simple Hashing Alone Isn’t Enough: The Rainbow Table Threat

Once you understand the peril of plain-text passwords, the next logical step is hashing. Hashing is a one-way cryptographic function that transforms an input (your password) into a fixed-size string of characters (the hash). The idea is that you can generate the same hash from the same input, but you can’t reverse the process to get the original input from the hash. So, when a user logs in, you hash their entered password and compare it to the stored hash. If they match, the password is correct.

Initially, many developers turn to fast, widely available hashing algorithms like MD5 or SHA-1. In the early 2000s, these seemed like robust solutions. But my experience quickly showed me their fatal flaw. Imagine a scenario where a database of hashed passwords (using, say, MD5) is leaked. A hacker doesn’t need to try and reverse the hash. Instead, they can use something called a rainbow table. A rainbow table is a precomputed list of common passwords and their corresponding hashes. Because MD5 and SHA-1 are fast and deterministic, generating such a table for a vast number of common passwords is trivial for attackers.

I vividly recall a penetration test where a client had used MD5 for all their password hashes. Within minutes of gaining access to their password database, I was able to crack over 60% of their user accounts using readily available rainbow tables. Why? Because hundreds of users had chosen simple passwords like “password123,” “qwerty,” or their own first name. The hashes for these common passwords were already in my rainbow table. It was like having a dictionary for cryptographic functions. The hashes looked like gibberish, but their meaning was instantly revealed because they were so common.

The critical insight here is that the speed of a hashing algorithm, while beneficial for general data integrity checks, becomes a security liability when used for passwords. A fast algorithm makes it easy for an attacker to generate a vast number of hashes quickly for their rainbow tables or brute-force attacks. Simple hashing, without further steps, is vulnerable to these precomputed attacks, especially for weak or common passwords. This is where the concept of ‘salting’ becomes indispensable.

The Power of Salts: A Unique Flavor for Every Password

To combat the rainbow table threat, security professionals introduced the concept of a salt. A salt is a unique, random string of characters added to a password before it’s hashed. This salted password is then hashed, and both the hash and the salt are stored in the database, usually alongside the username. When a user tries to log in, the system retrieves the stored salt, combines it with the entered password, hashes the combination, and compares it to the stored hash.

What changed everything for me about salting was understanding its elegant simplicity and profound impact. If every user has a unique salt, even if two users choose the exact same password (e.g., “password123”), their stored hashes will be completely different. Why? Because hash("salt1" + "password123") will produce a different output than hash("salt2" + "password123"). This immediately renders rainbow tables useless. An attacker can no longer precompute hashes for common passwords because each password now has billions of possible salted variations. They would need a rainbow table for every possible salt, which is computationally infeasible.

Consider an experience I had with a large enterprise client. Their old system, built a decade prior, used simple SHA-256 hashing. While better than MD5, it was still vulnerable to dictionary attacks if a breach occurred. When we upgraded their authentication system, we implemented unique, cryptographically random 16-byte salts for every single user. This meant that even if an attacker knew a specific user’s password was ‘secret123’, they couldn’t just look up SHA256('secret123') in a precomputed table. They’d need to know the specific salt, hash(salt + 'secret123'), and then compute that specific hash. This massively increases the effort required for an attacker. Instead of cracking millions of passwords with one giant table, they’d have to perform a dedicated brute-force attack for each individual password.

The key takeaway here is that a salt ensures that identical passwords result in unique hashes, effectively defeating generic precomputed attacks and forcing attackers to work significantly harder, password by password. This dramatically raises the bar for an attacker, turning a potential mass compromise into an arduous, time-consuming endeavor. Always use a strong, unique salt for every password.

Iterations and Key Stretching: Slowing Down the Attackers

While salting defends against rainbow tables, it doesn’t protect against brute-force attacks where an attacker tries millions or billions of password guesses. This is where key stretching (also known as password-based key derivation functions or KDFs) comes into play. The core idea is simple: intentionally make the hashing process computationally expensive. Instead of hashing a password once, you hash it many, many times in a loop, each time feeding the output of the previous hash back into the next iteration. This process is called iteration.

For example, if you hash a password 10,000 times, it takes 10,000 times longer to compute that single hash than if you hashed it once. For a legitimate user logging in, this delay is imperceptible – perhaps a few milliseconds. But for an attacker trying to crack a billion passwords per second, this seemingly small delay multiplies exponentially. Suddenly, their billion guesses per second become a hundred thousand per second, or even fewer, making large-scale brute-force attacks impractical.

I’ve seen clients initially resist this concept, arguing, “Won’t that slow down our login process?” The misconception is that it will grind their servers to a halt. My response is always to demonstrate the actual impact. Using a modern algorithm like bcrypt, which incorporates iterations, even with 12 rounds (roughly 4096 iterations), the login time for a single user is still well under 100 milliseconds on a standard server – far below what a human can perceive. However, if an attacker tried to crack millions of passwords offline, that 100-millisecond delay per password translates into weeks, months, or even years of computational effort.

What changed everything for me was seeing the raw numbers: a brute-force attack that might take an hour against a single-iteration SHA-256 hash could take an entire year against a properly iterated bcrypt hash. This massive increase in attacker effort is the entire point of key stretching. It buys time, makes attacks financially unviable, and protects even relatively weak passwords for a much longer period. When implementing password hashing, never skip the iterations. Algorithms like bcrypt and Argon2 are designed specifically for this, integrating salts and iterations seamlessly.

Choosing the Right Hashing Algorithm for Today and Tomorrow

With plain-text storage out, simple hashing vulnerable, and salts plus iterations firmly in place, the final critical decision is selecting the right hashing algorithm. This isn’t a static choice; algorithms are continually evaluated for their security against evolving attack methods and computational power. Algorithms like MD5 and SHA-1 are long deprecated for password storage due to their speed and known weaknesses. Even basic SHA-256/512, while cryptographically secure for other purposes, are too fast for password hashing unless heavily iterated with a custom KDF, which is error-prone to implement yourself.

In my practice, I strongly advocate for algorithms specifically designed for password hashing, primarily bcrypt and Argon2. Argon2 is generally considered the current state-of-the-art, having won the Password Hashing Competition in 2015. It offers configurable parameters to control memory usage and parallelism, making it highly resistant to both CPU and GPU-based brute-force attacks.

When guiding clients through their choices, the conversation often goes like this: “We’ve been using SHA-256, isn’t that good enough?” My explanation focuses on the ‘cost factor’ or ‘work factor’ that algorithms like bcrypt and Argon2 allow you to configure. With SHA-256, an attacker can throw immense computational power at it very efficiently. With bcrypt, you can set the number of ‘rounds’ or iterations, essentially telling the algorithm, “Make this many passes to generate the hash.” This directly translates to increased work for an attacker. With Argon2, you can specify not just iterations, but also the amount of memory and the number of threads (parallelism) it should use, making it uniquely resistant to attacks using specialized hardware like ASICs or GPUs.

The mistake I see most often is developers defaulting to general-purpose hashing functions for password storage. What changed everything for me was the clear distinction between a general-purpose hash (like SHA-256 for data integrity) and a password-specific KDF (like bcrypt or Argon2). The latter is built from the ground up to resist the specific types of attacks targeting passwords. Always choose a dedicated password hashing algorithm, and ensure you configure its work factor appropriately for your environment. For most new applications, Argon2 is the top recommendation, followed closely by bcrypt if Argon2 isn’t readily available in your programming language or framework.

Managing Algorithm Updates and Legacy Hashes

The landscape of security is constantly shifting. What’s considered secure today may not be sufficient in five or ten years. New attacks emerge, and computational power increases, making previously robust algorithms vulnerable. Therefore, a critical aspect of password security is having a strategy for updating your hashing algorithm without forcing all users to reset their passwords.

In my career, I’ve had to guide several organizations through this complex transition. The common scenario is an older system using, for instance, bcrypt with a low work factor or even an outdated algorithm. Forcing millions of users to reset passwords simultaneously is a massive undertaking, creates user friction, and often leads to users choosing weaker passwords out of frustration. The smart approach is a rolling upgrade.

What changed everything for me was realizing that you can store the hashing algorithm details (e.g., bcrypt$v=12$salt$hash or argon2id$v=19$m=4096,t=3,p=1$salt$hash) directly within the stored hash string itself. This allows your authentication system to know exactly how each password was hashed. When a user logs in, the system checks their stored hash:

  1. It identifies the algorithm and its parameters (e.g., bcrypt with 10 rounds).
  2. It hashes the entered password using those original parameters.
  3. If the hashes match, the user is authenticated.
  4. Crucially, after successful authentication, the system then re-hashes the password using the new, stronger algorithm and parameters (e.g., Argon2 with current recommendations) and updates the stored hash in the database. The next time that user logs in, their password will already be stored with the stronger algorithm.

This method allows for a graceful, user-transparent upgrade of your password security. Over time, as users log in, their passwords are automatically migrated to the stronger algorithm. New users are immediately hashed with the latest standard. For those users who don’t log in frequently, you might eventually need a forced reset for any remaining legacy hashes, but the vast majority will be upgraded seamlessly. This approach minimizes disruption and proactively strengthens your security posture without creating a massive user headache.

The key insight is to embed the hashing algorithm and its parameters directly into the stored hash string. This allows for backward compatibility and a phased migration strategy, ensuring your password storage remains up-to-date with minimal user impact. Regularly monitor security advisories and update your work factor or algorithm when needed.

Frequently Asked Questions

What is password hashing?

Password hashing is a one-way cryptographic function that transforms a password into a fixed-size string of characters, called a hash. Instead of storing the actual password, you store its hash. When a user attempts to log in, the system hashes their entered password and compares it to the stored hash. If they match, authentication is successful. This protects passwords from being read directly if a database is breached.

Why can’t I just encrypt passwords instead of hashing them?

Encryption is a two-way process: you can encrypt data and then decrypt it back to its original form with a key. While it might seem like a good idea, it means the decryption key must exist somewhere. If an attacker gains access to your database and the encryption key, they can decrypt all your users’ passwords. Hashing, being a one-way function, ensures that even if an attacker gets the hash, they cannot easily reverse it to get the original password, making it fundamentally more secure for password storage.

What is a salt in password hashing?

A salt is a unique, randomly generated string of data that is appended to a password before it is hashed. Each user’s password should have a unique salt. The purpose of a salt is to prevent rainbow table attacks, where attackers use precomputed hashes of common passwords to crack leaked databases. By using a unique salt for each password, even identical passwords will produce unique hashes, making precomputed tables ineffective.

What are key stretching and iterations?

Key stretching (or iterations) refers to intentionally making the password hashing process computationally intensive by repeating the hashing function many times. This makes it significantly slower for an attacker to perform brute-force attacks (trying many password guesses). While this adds a negligible delay for a legitimate user (milliseconds), it can turn an attacker’s attempt to crack millions of passwords from minutes into years, making the attack economically unfeasible. Algorithms like bcrypt and Argon2 incorporate key stretching automatically.

Which password hashing algorithm should I use?

For modern applications, you should use algorithms specifically designed for password hashing, such as Argon2 (the winner of the Password Hashing Competition) or bcrypt. Avoid older, faster, and less secure algorithms like MD5, SHA-1, or even basic SHA-256/512 (without a dedicated KDF implementation). These modern algorithms incorporate both salting and key stretching to provide robust protection against various attack types.

In the ever-evolving world of online security, password hashing isn’t just a best practice; it’s a fundamental necessity. The small amount of effort it takes to implement correctly pales in comparison to the immense risk of a data breach exposing clear-text user credentials. By understanding and applying the principles of salting, key stretching, and choosing modern algorithms like Argon2, you can significantly fortify your users’ data against the most common and devastating attacks. Make it a non-negotiable standard in your development workflow. Your users, and your reputation, depend on it.

Elias Vance — Security-minded generalist who writes about passwords, scams and account protection.

More briefs like this