DEV Community

Cover image for How to Store User Passwords Securely
Achal Tiwari
Achal Tiwari

Posted on

How to Store User Passwords Securely

Storing passwords securely is crucial for maintaining user privacy and data integrity. So, how should you store a password if you were asked to do so? The simplest way might seem to store it in plain text, but we all know that is not secure. Should we use encryption or hashing? Let's explore these methods together in our article today.
Image description

Plain Text Passwords

Alright, let's start with the most basic approach: plain text. Imagine you just write down all the passwords exactly as users enter them. It's simple, right? But here's the problem: if anyone gets access to this list, they can see all the passwords right away. It’s like leaving the keys to your house under the doormat. Not a good idea!

Encryption

Next, let's talk about encryption. Encryption is a process where we take a password and transform it into a different format using an algorithm. Think of it as a secret code. But here's the catch: encryption is reversible. If someone figures out the code (or gets the key, which you have to store somewhere), they can decrypt the password.

Image description

Experiment: Simple Encryption

Imagine we use something simple like ROT13, which just shifts each letter by 13 places in the alphabet. "password" becomes "cnffjbeq". But if someone knows we used ROT13, they can easily reverse it. Even with more complex encryption, if an attacker gets the key, they can decrypt all the passwords. So, reversible encryption isn't ideal for password storage.

Hash Functions

Now, let's move on to hash functions. Hashing takes a password and transforms it into a fixed-size string of characters, which looks nothing like the original password. The key here is that hashing is one-way: you can't easily reverse it to get the original password.

Image description

Experiment: Basic Hashing

Let's try using a simple hash function like SHA-256. Here's a quick example in Python:

import hashlib

password = "password123"
hash_object = hashlib.sha256(password.encode())
hashed_password = hash_object.hexdigest()
print(hashed_password)
Enter fullscreen mode Exit fullscreen mode

You’ll get a long, unique string. But even with secure hashes like SHA-256, there's a problem: they are very fast to compute. An attacker can try billions of passwords per second.

Enhancing Security with Salt, Pepper, and Iteration

To make things harder for attackers, we can add some extra layers of security: salt, pepper, and iteration.

Image description

Salt

A salt is a unique random value added to each password before hashing. This ensures that even if two users have the same password, their hashed passwords will be different.

Experiment: Adding Salt

import os

salt = os.urandom(16)  # Generate a random salt
password = "password123"
hash_object = hashlib.sha256(salt + password.encode())
hashed_password = hash_object.hexdigest()
print(hashed_password)
Enter fullscreen mode Exit fullscreen mode

Now, each time you run this, you'll get a different result because the salt is random. This makes it much harder for attackers to use precomputed hash tables.

Pepper

A pepper is a common secret value added to all passwords. Unlike salt, the pepper is stored separately from the database. This adds an extra layer of security because an attacker would need both the database and the pepper to crack the passwords.

Iteration

Iteration means hashing the password multiple times. This makes each hash calculation slower, which significantly increases the time required for attackers to crack passwords.

Experiment: Adding Iteration

iterations = 100000
hash = hashlib.sha256((salt + password.encode())).hexdigest()
for _ in range(iterations - 1):
    hash = hashlib.sha256(hash.encode()).hexdigest()
print(hash)
Enter fullscreen mode Exit fullscreen mode

By increasing the number of iterations, you make it more computationally expensive for an attacker to guess passwords.

Using Specific Functions

Instead of creating our own hashing algorithms, we can use established functions designed specifically for password storage, like bcrypt, scrypt, and Argon2. These functions already incorporate salt, iteration, and other security measures to resist attacks effectively.

Image description

Bcrypt

Let's dive into bcrypt, one of the most popular password hashing functions. Bcrypt is based on the Blowfish cipher and is designed to be slow to compute, making it harder for attackers to crack passwords.

Experiment: Using Bcrypt

Here's how you can use bcrypt in Python:

import bcrypt

# Generate a salt and hash a password
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(b'password123', salt)
print(hashed_password)

# Verify a password
is_correct = bcrypt.checkpw(b'password123', hashed_password)
print(is_correct)  # True
Enter fullscreen mode Exit fullscreen mode

With bcrypt, each hash includes a unique salt, and you can adjust the "work factor" to make the hashing process slower, enhancing security.

Why Bcrypt is Effective

Bcrypt’s strength lies in its ability to adjust the cost factor, which means you can make the hashing process more computationally intensive as hardware improves. This future-proofs your password security, making it more resistant to attacks over time.

Famous Cases of Hash Cracking

To understand why all this matters, let's look at some real-world examples:

  • LinkedIn (2012): LinkedIn used SHA-1 without salt, leading to the recovery of 90% of the leaked passwords in just three days.
  • Adobe (2013): Encrypted passwords without proper hashing led to a massive breach affecting 38 million users.
  • MySpace (2016): Use of unsalted SHA-1 hashes resulted in the compromise of 360 million accounts.

I hope this hands-on exploration helped you understand why and how to store passwords securely. Happy Coding!

Top comments (1)

Collapse
 
blessing_godfreysithole_ profile image
Blessing Godfrey Sithole

Thank you