Implementing Secure Password Hashing and Verification in Python

6 min read

📚 Quick Review: This practical application is built upon a fundamental programming concept. Review the Theory Lesson here first.


Introduction

In this practical lesson, we’ll walk through a Python code snippet that demonstrates how to securely hash and verify passwords using the PBKDF2_HMAC algorithm from Python’s built-in `hashlib` module. This implementation incorporates a unique salt and a high iteration count, crucial elements for robust password security.

The Code Snippet

Let’s start by examining the complete Python code:

import hashlib
import os

def hash_password(password: str) -> str:
    salt = os.urandom(16)
    key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
    return salt.hex() + ':' + key.hex()

def verify_password(stored_password: str, provided_password: str) -> bool:
    salt_hex, key_hex = stored_password.split(':')
    salt = bytes.fromhex(salt_hex)
    key = bytes.fromhex(key_hex)
    new_key = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000)
    return new_key.hex() == key_hex

Step-by-Step Code Breakdown

Let’s dissect each part of this code to understand its functionality and the security principles behind it.

Importing Necessary Modules

import hashlib
import os
  • `import hashlib`: This module provides various secure hash and message digest algorithms, including PBKDF2. It’s the core of our cryptographic operations.
  • `import os`: This module provides a way of using operating system dependent functionality. We specifically use `os.urandom` to generate cryptographically strong random bytes for our salt.

The `hash_password` Function

This function takes a plain-text password and returns a securely hashed version suitable for storage.

def hash_password(password: str) -> str:
  • This defines the function `hash_password` which accepts a string `password` and is type-hinted to return a string.
    salt = os.urandom(16)
  • `salt = os.urandom(16)`: Here, a 16-byte (128-bit) cryptographically strong random value is generated using `os.urandom()`. This `salt` is unique for each password and is crucial for preventing rainbow table attacks and ensuring that identical passwords result in different hashes.
    key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
  • `key = hashlib.pbkdf2_hmac(…)`: This is the core hashing operation. Let’s break down its arguments:
    • `’sha256’`: Specifies the underlying hash algorithm to be used by HMAC. SHA256 is a strong, widely accepted cryptographic hash function.
    • `password.encode(‘utf-8’)`: The password string must be converted into bytes before being passed to cryptographic functions. UTF-8 encoding is standard.
    • `salt`: The unique 16-byte salt generated earlier.
    • `100000`: This is the iteration count (or cost factor). It dictates how many times the hashing process is repeated. A higher number makes brute-force attacks significantly slower and more expensive. 100,000 is a reasonable starting point, but this value should be benchmarked and potentially increased over time as computing power advances.
  • The result, `key`, is the derived key (the hashed password) in bytes.
    return salt.hex() + ':' + key.hex()
  • `return salt.hex() + ‘:’ + key.hex()`: The salt and the derived key are both in bytes. To store them as a single string, they are converted to their hexadecimal representations using `.hex()` and then concatenated with a colon (`:`) separator. This combined string is what you would store in your database.

The `verify_password` Function

This function compares a provided password with a stored hashed password to check for a match.

def verify_password(stored_password: str, provided_password: str) -> bool:
  • This defines the function `verify_password` which takes the `stored_password` (the combined salt and hash string) and the `provided_password` (the plain-text password entered by the user) and returns a boolean.
    salt_hex, key_hex = stored_password.split(':')
  • `salt_hex, key_hex = stored_password.split(‘:’)`: The stored combined string is split back into its hexadecimal salt and key components using the colon separator.
    salt = bytes.fromhex(salt_hex)
    key = bytes.fromhex(key_hex)
  • `salt = bytes.fromhex(salt_hex)`: The hexadecimal salt string is converted back into its original byte format.
  • `key = bytes.fromhex(key_hex)`: Similarly, the hexadecimal key string is converted back to bytes. While `key` itself isn’t strictly used for comparison in the next step, converting it back demonstrates the full round trip. The `key_hex` string is sufficient for the final comparison.
    new_key = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000)
  • `new_key = hashlib.pbkdf2_hmac(…)`: This is the critical step for verification. The `provided_password` is hashed using the exact same salt and iteration count that were used to hash the original password. This ensures that the hashing process is consistent.
    return new_key.hex() == key_hex
  • `return new_key.hex() == key_hex`: The newly derived `new_key` (from the provided password) is converted to its hexadecimal string representation and then compared with the `key_hex` (the stored hashed password’s hexadecimal part). If they match, the provided password is correct; otherwise, it’s incorrect.

Execution Environment and Usage

This code can be run in any standard Python 3 environment. You would typically integrate these functions into your user authentication system.

Here’s a simple example of how to use these functions:

# Example Usage:

# 1. User registration (hashing the password for storage)
user_password = "mySuperSecretPassword123!"
hashed_password_for_db = hash_password(user_password)
print(f"Hashed Password (for storage): {hashed_password_for_db}")

# In a real application, you would store 'hashed_password_for_db' in your database

# 2. User login (verifying a provided password)
# Simulate retrieving the stored hash from the database
stored_hash = hashed_password_for_db

# Scenario A: Correct password provided
provided_password_correct = "mySuperSecretPassword123!"
is_correct = verify_password(stored_hash, provided_password_correct)
print(f"Is the correct password provided? {is_correct}") # Expected: True

# Scenario B: Incorrect password provided
provided_password_incorrect = "wrongPassword"
is_incorrect = verify_password(stored_hash, provided_password_incorrect)
print(f"Is an incorrect password provided? {is_incorrect}") # Expected: False

When a user registers, you would call `hash_password()` and store its return value in your database. When a user attempts to log in, you would retrieve the stored hash for their username, then call `verify_password()` with the stored hash and the password they just entered. The boolean result tells you if they provided the correct password.

💡 Developer Tip: Never store plain text passwords. Always use a strong, modern key derivation function like PBKDF2, bcrypt, or scrypt. Avoid deprecated methods like simple MD5 or SHA1. Ensure your iteration count is high enough to make brute-force attacks computationally expensive, but not so high that it impacts user experience too severely.

Conclusion

By implementing these `hash_password` and `verify_password` functions, you establish a secure foundation for handling user credentials in your Python applications. The use of unique salts and a high iteration count with PBKDF2_HMAC significantly enhances the security against common password cracking techniques, protecting your users and your application from potential breaches. Remember that security is an ongoing process, and staying updated with best practices is key.

Leave a Reply

Your email address will not be published. Required fields are marked *