Storing and Retrieving Passwords Securely in Python

Storing And Retrieving Passwords Securely In Python

Passwords provide the first line of defence for securing access to applications and online accounts. As developers, we have a responsibility to store user passwords securely. Improper password storage can put users’ personal information and data at risk.

In this comprehensive guide, we’ll cover Python’s best practices for securely saving passwords to a database and retrieving them for authentication. Whether you’re building a new application or improving security in an existing system, this guide has you covered.

Also read: Random Password Generator in Python | GUI Tkinter

Why Password Security Matters

When an application needs to authenticate users, it must have some way to verify their identity. The most common method is asking the user to provide a username and password. These passwords serve as sensitive “keys” that grant access.

If poorly stored, passwords can be stolen in database breaches. Hacked databases are sold online, exposing users’ credentials. With a stolen password, attackers can:

  • Log into the user’s account to steal private data
  • Access integrated third-party accounts like email and financial services
  • Commit identity fraud using personal information from breached accounts

Securing user passwords helps mitigate these risks. Proper password storage also builds user trust in your application.

Core Concepts of Secure Data Storage

Before diving into Python code, let’s review some core concepts for safely handling user passwords:

Hash Functions

A cryptographic hash function encodes data in a one-way, irreversible format. The same input always produces the same fixed-length output. But there is no way to “reverse” the output and reveal the original input.

This allows matching passwords without actually storing passwords. When a user signs up, their password is hashed. The hash value is stored instead of the plain text password. Later during login, the user-entered password is hashed again. If the newly generated hash matches the stored hash, the passwords are the same.

Also read: How Do I Return Hash Values of a 1D array?

Salts

Most users have common passwords like “123456” and “password”. This makes hash collisions more likely for identical passwords.

salt is random data added to each password hash. Salting ensures every hashed password has a unique value, even if the plain text passwords are identical. This protects against precomputed “rainbow table” attacks that crack hashed passwords.

Iterations

Performing computationally intensive iterations of hash functions further slows brute force attacks. With key derivation functions like PBKDF2, we can configure hundreds of thousands of hash iterations. This massively delays dictionary and brute force attacks.

Pepper

pepper is an application-wide secret value added to password hashes for extra protection. Unlike salts which are unique per user, the pepper is the same across all passwords. It provides an additional obstacle for leaked password databases. If the pepper isn’t known, brute forcing hashes is extremely difficult.

Python Password Storage Process

Equipped with those core concepts, let’s outline the secure password workflow:

  1. User signs up and enters a password
  2. Generate a long, random salt value
  3. Optionally append a standard pepper value
  4. Hash the salted password thousands of times
  5. Save the salt, hash iterations, hash value to the user’s database record

When authenticating later:

  1. Retrieve the user’s salt, iterations, and hash value from the database
  2. Salt and hash the entered login password using the same parameters
  3. Compare the login hash to stored hash to check for match

Next, we’ll explore Python code implementations for each step.

Generating Random Salts

Here is function to create a cryptographically-secure 16 byte (128 bit) random salt value:

import os

def generate_salt():
  # 128 bit salt value 
  salt = os.urandom(16) 
  return salt

The os.urandom() function from Python’s standard library generates secure random byte values. We take 16 random bytes for a 128 bit salt.

Alternatively, the secrets module introduced in Python 3.6 provides a secrets.token_bytes() method for generating secure random data.

Salts should have sufficient randomness to make collisions unlikely. Longer byte lengths are ideal to prevent rainbow table attacks tailored to shorter salts.

Hashing Passwords

Python’s hashlib module implements standardized cryptographic hash functions for securely encoding text and bytes.

For example, here is how to hash a password with SHA-256:

import hashlib
import os

def generate_salt():
  # 128 bit salt value 
  salt = os.urandom(16) 
  return salt


def hash_password(password):
  salt = generate_salt()  
  password_hash = hashlib.sha256(salt + password.encode())  
  return salt + password_hash.digest() 


print(hash_password("HelloWorld"))

We salt each password with the random generate_salt() value. hashlib.sha256() expects byte input, so the password string is encoded. Calling .hexdigest() returns the hash value as a hexadecimal string.

The salt bytes concatenated with the hexadecimal digest is the final salted, hashed password. We’ll split these apart later during verification.

SHA-256 offers decent protection for passwords. For extra security, key derivation functions like PBKDF2 hash thousands of times to resist brute force cracking.

Key Derivation with PBKDF2

Python’s hashlib implements PBKDF2 key derivation as hashlib.pbkdf2_hmac(). The example below handles user sign up, generating a secure salted hash with 100,000 PBKDF2 iterations:

import os
import hashlib

def create_secure_password(password):
  salt = os.urandom(16)
  iterations = 100_000  
  hash_value = hashlib.pbkdf2_hmac(
    'sha256',  
    password.encode('utf-8'), 
    salt, 
    iterations
  )
  password_hash = salt + hash_value
  return password_hash

print(create_secure_password("HelloWorld"))

We pass the hash algorithm, password, salt, and iterations into hashlib.pbkdf2_hmac(). It returns the derived key as bytes. The final password hash concatenates the random salt and hash value.

Output

b'h{\xa5\xa4\xf3\xa3\xd9{\xbe\xc2Ai\x07\xa1\xf8\x0bu;\xce1\x9d\xc4,\xd0WH\xc2#1!\x07\x06\xac\x18\xfa\x15\xf0\x84\xe5q+\xdbZ\x9a\x02\xc0\xbd\xcc'

PBKDF2 massively slows brute force attacks compared to single-round hashes. 100,000 iterations add just 150 milliseconds delay – negligible for users, but a lifetime for attackers!

Adding an Application-Wide Pepper

We can require a standard pepper value across all password hashes for additional safety. This pepper is an extra secret attacker must know to crack hashes.

Here is an updated example using a pepper named SECRET_KEY:

import os
import hashlib
 
PEPPER = "SECRET_KEY"
 
def create_secure_password(password):
  salt = os.urandom(16)  
  iterations = 100_000
  hash_value = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8') + PEPPER.encode('utf-8'),  
        salt,
        iterations
  )  
  password_hash = salt + hash_value
  return password_hash 


print(create_secure_password("HelloWorld"))

We concatenate the pepper to the user’s password before hashing. Now, an attacker must discover both the pepper value and salt to brute force a match.

Output:

b'\x07Z\x1a\xbf\xf5)C\xc9u9\xa3\xfa@\xd6\xb2\xb9D\xf2YR\'\xb7\x9d\xbb\x07I\xde\x8fe\x86@"\xb9e/\xb3\xc4zp\x1b\xaa~\x8e]\x8a\x9c\x88X'

Peppers provide an extra layer of protection if your hashed passwords are leaked. They are toughest to crack when the pepper value differs from salts and is unknown to attackers.

Storing Hashed Passwords using Python and MySQL

With secure hashing functions implemented, let’s save user passwords. Hashed passwords should be stored in a database table alongside related authentication details like:

  • Random salt value
  • Hash algorithm name
  • PBKDF2 iterations
  • Username or ID foreign key

Here is an example user accounts table schema with a password_hash column:

CREATE TABLE accounts (
  id INT PRIMARY KEY,
  username VARCHAR(50),
  password_hash VARCHAR(500), 
  salt VARCHAR(100),
  hash_algo VARCHAR(10),
  iterations INT
);

When a new user signs up, we’ll run our hashing procedure and insert hash details into the table:

import mysql.connector

db = mysql.connector.connect(
  host="localhost",
  user="appuser",  
  password="dbpassword",
  database="users"
) 

cursor = db.cursor()

def create_user(username, password):
  
  # Generate secure hash
  password_hash = create_secure_password(password)  
  
  # Split hash into components
  salt, key = password_hash[:16], password_hash[16:]
  hash_algo = "PBKDF2"
  iterations = 100_000 

  # Insert into database
  insert_sql= (
     "INSERT INTO accounts (username, password_hash, salt, "
     "hash_algo, iterations) "
     "VALUES (%s, %s, %s, %s, %s)"
  )

  cursor.execute(insert_sql, (
     username,
     key,
     salt,
     hash_algo,
     iterations
  ))

  db.commit() 


The user’s login credentials are securely saved in the database, ready for verification during login.

Authenticating Users using Secured Password Hashes

When users attempt to log in with their password, we’ll fetch their password hash details from the database.

We re-hash the entered password using the original salt, algorithm, and iterations. We can verify if the password matches after comparing the newly generated hash to the stored hash.

Here is an example login code:

import mysql.connector
import hashlib

db = mysql.connector.connect(
  host="localhost",
  user="appuser",
  password="dbpassword",  
  database="users"
)

cursor = db.cursor()

def login(username, password):

  # Get hash details from database 
  select_sql = "SELECT * FROM accounts WHERE username = %s"
  
  cursor.execute(select_sql, (username,))
  account = cursor.fetchone()
  
  if not account:
    print("Invalid username")
    return

  salt, key, hash_algo, iterations = account[2:6]
  
  # Recompute hash from user entered password  
  password_hash = hashlib.pbkdf2_hmac(
    hash_algo, 
    password.encode('utf-8'),  
    salt,
    iterations
  )

  # Compare hashes
  if password_hash == key:
    print("Login successful")
  else:
    print("Invalid password")


We reuse the original salt, algorithm, and iteration count to re-hash the entered password. Now, comparing hashes rather than plain text we can rest assured that the actual passwords are never exposed.

This allows verifying logins without storing or exposing real passwords. Even if your database is breached, properly hashed values are difficult for attackers to reverse.

Summary

Securely handling user passwords is crucial for building trust in any application. Never store plain text passwords – instead, properly hash and salt them.

Python’s standard library has excellent tools for password security:

  • os.urandom and secrets.token_bytes generate cryptographically-secure random salt values
  • hashlib provides access to fast, modern hashing functions like SHA-256
  • PBKDF2 hashes passwords hundreds of thousands times resisting brute force
  • Peppers provide application-wide secrets protecting leaked hashes

With hashes safely stored in the database, users are protected even in the event of a breach. With these best practices, we can authenticate users without ever handling their real plain text passwords.