DEV Community

Cover image for Storing Passwords Securely - A Journey from Plain Text to Secure Password Storage
Shivram Sambhus
Shivram Sambhus

Posted on • Originally published at shivi.io

Storing Passwords Securely - A Journey from Plain Text to Secure Password Storage

From Plain Text to Secure Password Storage

Password storage has come a long way since its early days as an afterthought in computing. In the past, passwords were often stored without encryption, making them vulnerable to unauthorized access. However, storing passwords securely is no longer enough; it's equally important to ensure that they're stored in a manner that limits their potential impact if compromised.

In this post, we'll delve into the evolution of password storage, from its insecure origins to modern practices that prioritize user safety and security.

The Naive Approach: Storing Secrets in Plain Text

How it Works (Don't Try This at Home!)

Imagine you're storing passwords in a database table called users. Each row represents a user, and the password column contains their login credentials. In this naive approach, you simply store the password as is, without any encryption or hashing.

For example, if John Smith's password is "Sup3rSecure123", his entry in the users table would look like this:

username password
johnsmith Sup3rSecure123

Why This Approach is a Recipe for Disaster

Storing passwords in plain text may seem convenient, but it's an open invitation to attackers. If an attacker gains access to your database or file system, they can simply read the password column and gain unauthorized access to all user accounts. This approach also makes it trivial for malicious insiders or developers to obtain users' passwords.

It was the 2000s, and a popular social networking site had stored its users' passwords in plain text. One day, an attacker managed to gain access to their database and stole all the passwords. The attackers then used these stolen credentials to log in to the site and steal sensitive information from millions of users.

If only the developers had used hashing to store the passwords, this disaster could have been avoided. But they didn't, and the consequences were severe. (Myspace 2008)

Storing other sensitive information in plain text

You should also avoid storing other sensitive information in plain text, such as API keys or access tokens in plain text in your codebase. If an attacker gains access to your source code or configuration files, they can easily extract these secrets and use them to compromise your systems. Instead, consider using secure storage solutions like environment variables, configuration files, or secret management tools to store sensitive information securely.

Insecure API Keys

Hashing Passwords: A Step in the Right Direction

You realized that storing plain text passwords isn't a good idea, so what do you do instead? The next logical step would be scrambling or "hashing" the password. Hashing is a fundamental step that can significantly improve security.

Hashing... What's That?

Imagine you're hosting a party with special guests. You want to make sure only the right people get in, so you create a secret system to check their names. You take each name and run it through a machine that scrambles the letters into a unique code. That's kind of like what hashing does for passwords!

Hashing is a way to take an input (like a password) and turn it into a fixed-size output (the hashed password). It's like a secret code that can only be read one way, so you can't figure out the original password from the hashed one. There are different hashing algorithms, Sha-256 is pretty popular. It generates a 256-bit hash value, which is a long string of characters that looks like random gibberish.

# Example
sha256("MyPassword") -> 0xdc1e7c03e162397b355b6f1c895dfdf3790d98c10b920c55e91272b8eecada2a
Enter fullscreen mode Exit fullscreen mode

Example of an authentication system using hashing:

import hashlib
import getpass

secrets = {}

def hash_password(password):
    return hashlib.sha256(password.encode()).hexdigest()

def save_password(username, password):
    hashed_password = hash_password(password)
    secrets[username] = hashed_password

def authenticate(username, password):
    stored_password = secrets.get(username)
    if stored_password and stored_password == hash_password(password):
        return "Authentication successful"
    else:
        return "Authentication failed"

# Example usage
username = input("Enter your username: ")
password = getpass.getpass("Enter your password: ") # Hide the password input from the terminal
save_password(username, password)

print(authenticate(username, password))  # Output: Authentication successful
Enter fullscreen mode Exit fullscreen mode

You're on the Right Track, But...

You might think that hashing is enough to keep your passwords safe, but unfortunately, there are some serious vulnerabilities lurking in the shadows. While hashing is an essential step towards securing passwords, it's not a foolproof solution on its own.

One major weakness is rainbow tables, which are precomputed lists of common passwords and their corresponding hash values. If an attacker gains access to your hashed passwords, they can simply look them up in the table and recover the original password. This is known as a dictionary attack, and it's a popular way for hackers to crack hashed passwords.

Another issue is collision attacks, where two different passwords produce the same hash value. Imagine having two users with the same password - if their hashed passwords are identical, an attacker can exploit this vulnerability to log in as either user using the same hash value. This is a critical security flaw that can compromise your entire authentication system.

Finally, there's the threat of brute-force attacks, where powerful computers are used to try different combinations of characters until they find the right password. Even with a robust hashing algorithm like SHA-256, attackers can still use their computational power to crack hashed passwords in a relatively short amount of time.

It's essential to recognize that hashing itself is not flawed, rather it's how hashing is implemented for password storage that's the problem. By combining hashing with additional security measures, you can create a more robust password storage system that keeps your users' sensitive information safe from prying eyes.

Adding Some Flavor to Your Passwords with Salting

To address the vulnerabilities of plain hashing, you can introduce a technique called salting. Salting involves adding a unique random value to each password before hashing it. By adding a unique random value (salt) to each password before hashing, we're making it way harder for attackers to use precomputed tables or find collisions. Because who wants their passwords to be easily searchable online or accidentally match someone else's because they chose the same weak password?

import hashlib
import getpass
import os

secrets = {}

def generate_salt():
    return os.urandom(16).hex()

def hash_password(password, salt):
    hashed_password = hashlib.sha256((password + salt).encode()).hexdigest()
    return hashed_password

def save_password(username, password):
    salt = generate_salt()
    stored_password = hash_password(password, salt)
    secrets[username] = {'salt': salt, 'hashed_password': stored_password}

def authenticate(username, password):
    stored_credentials = secrets.get(username)
    if stored_credentials and stored_credentials['salt'] == generate_salt() and stored_credentials ['hashed_password'] == hash_password(password, stored_credentials['salt']):
        return "Authentication successful"
    else:
        return "Authentication failed"

# Example usage
username = input("Enter your username: ")
password = getpass.getpass("Enter your password: ")
save_password(username, password)

print(authenticate(username, password))  # Output: Authentication successful
Enter fullscreen mode Exit fullscreen mode

While salting is effective in preventing collisions and defending against rainbow table attacks, it's not a foolproof solution. Modern computers can perform brute-force attacks quickly, making commonly used passwords vulnerable to extraction. For example, popular tools like Hashcat and John the Ripper can crack salted and hashed passwords by trying different combinations of characters at high speeds.

Expensive Algorithms

The next logical step in securing your password storage is to use algorithms that are specifically designed to resist brute-force attacks. These algorithms are designed to be slow and computationally expensive, making it much harder for attackers to perform brute-force attacks. Some examples of such algorithms include:

  • Argon2: A widely-used password hashing algorithm that is designed to be slow and resistant to parallelization.
  • PBKDF2: A password-based key derivation function that uses a salt value and multiple iterations of a hash function to generate a derived key.
  • Bcrypt: A popular password hashing algorithm that uses a combination of algorithms, including Blowfish and SHA-256.
import bcrypt
import getpass

# Generate a hash for the password "hello"
hash = bcrypt.hashpw("hello".encode('utf-8'), bcrypt.gensalt())

print(hash)

# Check if the provided password matches the stored hash
def check_password(stored_hash, provided_password):
    return bcrypt.checkpw(provided_password.encode('utf-8'), stored_hash)

password = getpass.getpass("Enter your password: ")

if check_password(hash, password):
    print("Password is correct")
else:
    print("Invalid password")
Enter fullscreen mode Exit fullscreen mode

By using these algorithms, you can significantly increase the time and resources required to crack hashed passwords, making it much harder for attackers to compromise your system. Bcrypt, for example, allows you to configure the cost factor, which determines how many rounds of hashing are performed. The higher the cost factor, the more computationally expensive the hashing process becomes. One hash can take hundreds of milliseconds to compute, making it much harder for attackers to crack passwords using brute-force attacks.

This is the industry standard for storing passwords securely. By using a combination of salting, hashing, and expensive algorithms, you can create a robust password storage system that protects your users' sensitive information from unauthorized access.

Enhancing Password Security with Peppers

Now that you've learned about salting, hashing, and expensive algorithms, you might be wondering if there are additional ways to enhance password security. Well, if you're a small startup or a personal user, the techniques we've discussed so far might be sufficient. But if you're a billion-dollar company or a government agency, you might want to consider upping your game.

Let's say you're a company storing credentials to some VIP accounts - one of them is using the password "password123". If an attacker gains access to your database and wants specifically to target this VIP account, they can use a brute-force attack to crack the password, even with the expensive algorithms. It might take some time, but it's still possible.

The reason this is possible is because all the information needed to validate the password is stored in the database. So what we do is add an additional variable which is not stored in the database, but is used in the hashing process. This is called a pepper.

What are Peppers?

Think of peppers as similar to salts, but with a twist. While salts are unique per user, peppers are shared across all users. This means that the pepper value is constant and not stored in the database. Instead, it's kept in a secure location, such as a configuration file or a Hardware Security Module (HSM) (more on that later).

For example, let's say you have a password "MyP@ssw0rd" with a salt value of "SALT123". If you add a pepper value of "PEPPER456", your hashed password would look something like this: hash(MyP@ssw0rd + SALT123 + PEPPER456). This makes it virtually impossible for attackers to crack the password, even if they have access to the database. Without the pepper value, the hashed password is meaningless and cannot be used to validate the original password.

What are Hardware Security Modules (HSMs)?

HSMs in simple terms are specialized hardware devices that are designed to securely store sensitive information, such as encryption keys, certificates, and passwords. They provide a secure environment for cryptographic operations and can be used to protect your password hashing process from unauthorized access.

In the context of password storage, you can use an HSM to securely store your pepper value, making it virtually impossible for attackers to access or tamper with it. By using an HSM, you can ensure that your pepper value remains secure and protected from unauthorized access.

And therefore we conclude...

Storing passwords in plain text is amazing. I mean, who doesn't love making it easy for hackers to get their hands on sensitive info? But seriously, using expensive hashing algorithms which internally combine salting is the way to go. And if you're a big shot, you might want to consider adding a pepper to the mix and storing it in an HSM. This way, you can sleep soundly knowing that your users' passwords are safe and secure.

Thanks for reading! May your passwords always remain secure... until the AI uprising, of course :D

This article was originally posted on: www.shivi.io/blog/password-storage

Meme

Top comments (0)