The Evolution of Password Security: From Basic Storage to Argon2

The Evolution of Password Security: From Basic Storage to Argon2

Written by Francesco Di Donato July 18, 2025 10 minutes reading

Whether you write the code or some Artificial Intelligence writes it for you, it’s fundamental to have a clear understanding of the implementation. This is even more true for such a crucial part of an application as authentication.

Today, many modern authentication methods exist, such as OAuth, Magic Links, and WebAuthn. These are perfectly valid and, in fact, often recommended choices. However, there are still a vast number of applications that prefer passwords, for example, custom-built management systems for businesses. The combination of email and password is, in this sense, the “crocodile” of authentication: it existed before other methods and will probably still be around after them.

However, this doesn’t mean that email and password haven’t evolved over time. Like any system exposed to the environment and its threats (hackers), this pair is forced to mutate in search of solutions.

Cleartext Passwords

Let’s start from the beginning, the simplest, “naive” solution. Imagine: you are the server, the user sees a form on the page. They enter their email and password, press submit, and that data arrives at your server. You take it and, just as it arrived, you put it into the database.

When the user wants to log in, they provide you with the data again, and you check that the two passwords are the same.

# Registration
registration_password = "123456" # <- Save to DB

# Login
login_password = "123456"

is_auth = login_password == registration_password # Yeah!

But I’m saving in cleartext. Very bad. Why?

Database Leaking

Servers and databases are like castles that can be protected better and better. However, they interact with the outside world and therefore always have a “loophole.” Databases are “leaked” much more often than you might think.

If a server saves passwords in cleartext in a Database and its contents are exposed, anyone with access to the leaks can, without any obstacle, access accounts as your users.

The real disaster, however, is that people reuse passwords. Statistics show that over 50% of people use the same password for multiple services. This means that if your small site suffers a data leak, hackers will try those same credentials (email and password) on platforms like banks, social networks, or email services… a true chain reaction disaster.

Hashing

The solution lies in not saving passwords directly in the Database, but rather their “fingerprint”. A long string of pseudorandom characters, which appear random but in reality depend directly on the input (the original password).

It’s important not to confuse hashing with encryption. Encryption is a reversible process: what is encrypted can be decrypted. Hashing, however, is one-way. And that’s exactly what we want for passwords: a system that can verify them, but never reveal them.

These hashes are produced using cryptographic hashing functions. Such functions are “one-way”: it’s easy and fast to calculate the hash from the password, but it’s computationally impossible to reverse engineer the original password from the hash. If you’re curious to intuitively understand how one of these works, you can read my interactive article on SHA256.

import os
import hashlib

# Registration
p1 = "123456"
p1_hashed = hashlib.sha256(p1.encode()).hexdigest() # "8d9...c92" <- Save to DB

# Login
p2 = "123456"
p2_hashed = hashlib.sha256(p2.encode()).hexdigest() # "8d9...c92"

is_auth = p2_hashed == p1_hashed # Yeah!

When the user sends you the password, you don’t record it in cleartext anywhere. You pass it directly to your hashing function which, very quickly (but we’ll see that this speed can be a problem), will generate the resulting fingerprint. This is saved in the database along with the email.

When the user attempts to log in, you perform the same hashing operation with the provided password. The comparison will then happen on the two fingerprints.

When the hacker gains access to the Database content, they won’t simply be able to go to the login page and enter the hash. The system expects the original password. If the hacker sends the hash itself, the system (which doesn’t “think,” but simply executes) will take what is already a hash and calculate another one, which obviously will never match the one generated from the original password.

Here, the concept of a password evolves into a trinity:

  • The original password, in cleartext
  • The chosen hashing algorithm (e.g., SHA-256 is actually not optimal for this purpose, explained below)
  • Technically also the seed of the hashing function

Alright, passwords are now protected by hashes. But is your user safe? Not yet.

Rainbow Table

The problem is that a hash function is deterministic: the same input always produces the same output. If two users use the same password “password123”, they will have the exact same hash in the database.

Furthermore, the SHA-256 function belongs to the family of fast and efficient ones. And in this case, speed becomes a vulnerability.

It is said that “my cousin” (or, more realistically, well-equipped attackers) used a large number of GPUs in parallel on a huge quantity of common passwords that people habitually use (like 123456, password1!, qwerty, and even more complex combinations).

In offline mode, they precompute all the hashes for this massive amount of passwords. Once generated, the table no longer requires electricity consumption for lookups. The result is a gigantic lookup table, known as a Rainbow Table.

Now, the hacker takes the hashes from your database, chooses one, and checks the Rainbow Table to see if that hash was precomputed. If they find it (and they do so in constant time thanks to the hash map implementation), they can instantly trace back to the original input that generated that hash. An input which, of course, matches the cleartext password! Now the hacker can log in using the user’s credentials!

This is exactly what happened in the famous 2012 LinkedIn data breach.

Salt

How do you render a pre-calculated table like this useless? Simple: by ensuring that every password, even if identical to another, produces a completely different hash.

The solution is called Salt. It’s a random and unique string of characters, generated for each user at the time of registration. This string is added to the password before performing the hashing.

import os
import hashlib

# Registration
p1 = "123456"
salt1 = os.urandom(16).hex() # -> 'a4b8c7...' (a random and unique salt)
p1_salted = p1 + salt1
p1_hashed = hashlib.sha256(p1_salted.encode()).hexdigest() # <- Save to DB p1_hashed and salt1

# Login
p_login = "123456"
# Retrieve salt1 from DB for this user
p_login_salted = p_login + salt1
p_login_hashed = hashlib.sha256(p_login_salted.encode()).hexdigest()

is_auth = p_login_hashed == p1_hashed # Yeah!

The process becomes:

“But wait,” you might say, “if the hacker steals the database, they steal both the hashes and the salts. What’s the point?”

Excellent question! The salt, in fact, is not a secret. Its strength lies not in its secrecy, but in its uniqueness. Since each user has a different salt, “my cousin’s” Rainbow Table becomes an expensive paperweight. To attack your database, the hacker would have to create a new, gigantic Rainbow Table for each individual user, using their specific salt. This makes the attack economically and computationally unsustainable, reducing it to an individual brute-force attack, password by password, which is infinitely slower.

Slower Hashing Function

We’ve rendered Rainbow Tables useless, but we’re not done yet. The hacker can still take a single user (with their hash and salt) and try to guess their password with a brute-force attack.

Here the speed problem reappears. Algorithms like SHA-256 are designed to be lightning-fast. With a good GPU, an attacker can test billions of combinations per second. If a user’s password is “juventus1990”, even with the salt, it will be found in a few minutes.

The solution is a paradigm shift: use deliberately slow and “expensive” hashing algorithms in computational terms.

Imagine no longer using a kitchen blender, but a complex industrial machine that takes half a second to process a single password. For a user logging in, half a second is imperceptible. For a hacker who has to try billions of passwords, however, this slowdown makes the attack prohibitive.

This is where specific password algorithms come into play:

Timing Attack Prevention

The simple comparison hash1 == hash2 can expose a subtle but real risk called a Timing Attack. To avoid this, it’s crucial to use a “constant-time” comparison function, which always takes the same amount of time to return a result. In Python, for example, the hmac library offers the compare_digest function.

import hmac
is_auth = hmac.compare_digest(p_login_hashed, p1_hashed) # Much more secure!

Pepper

There’s an additional layer of “paranoia” we can add to increase security. We have the hash and the salt in the database. What happens if a hacker manages to breach the server and perform a complete DB dump? They’ll have everything they need to start their slow offline brute-force attacks.

We can make their life even harder with a Pepper. The pepper is a secret value, a static string known only to the application. Unlike the salt, the pepper is not saved in the database, but is hidden in the application’s source code or in a secure configuration file on the server.

The calculation becomes: hash(password + salt + pepper).

If the hacker steals the database, they’re still missing a piece of the puzzle: the secret pepper. Without it, they cannot replicate the hashes, rendering their database dump almost useless. This represents an additional line of defense against a total database compromise.

The pepper increases security, but it’s not a panacea. If a hacker gains access not only to the database but also to the server’s source code or configuration files, they will also find the pepper, nullifying its usefulness. Furthermore, managing the pepper (its rotation, its distribution in multi-server environments) can be complex. It’s an excellent additional layer, but it doesn’t replace the need for a slow hashing algorithm and a salt.

Passwordless

We have built an almost impregnable fortress for our passwords. But the truth is, the most secure fortress is one that doesn’t need to be defended. The ultimate evolution is passwordless authentication.

The idea is to completely eliminate the user’s need to create and remember a password. Techniques include:

However, as long as the classic email/password combination continues to exist, our duty is to protect it in the most robust way possible: by using a modern, slow, and memory-hard hashing algorithm like Argon2id, a unique salt for each user, and, for maximum security, a secret pepper.