Python’s Bcrypt Library

Overview

Python’s bcrypt library is a highly secure way of handling password hashing and verification. It’s a wrapper around the OpenBSD bcrypt implementation, designed specifically to hash passwords in a secure way. The bcrypt hashing algorithm is resistant to rainbow table and brute force attacks because of its adaptive nature, meaning it becomes slower as computational power increases.

Why Use bcrypt?

  • Security: Designed specifically for password hashing with high computational cost to mitigate brute force attacks.
  • Salt: Automatically generates a salt to avoid using the same hash for identical passwords.
  • Adaptive: Allows increasing the complexity of hashing as computational power improves.
  • Cross-Platform: Available on multiple operating systems and Python versions.

Key Features

Hashing

  • Password Hashing: Securely hash passwords for storage.
  • Adaptive Work Factor: Adjusts the computational effort needed for hashing.

Verification

  • Password Verification: Check if a given password matches a stored hash.

Utility Functions

  • Salt Generation: Create random salts for password hashing.
  • Hash and Salt Configuration: Configure the strength and salt length of hashes.

Installing bcrypt

To start using bcrypt, you’ll need to install the library. You can do this via pip:

pip install bcrypt

Hashing Passwords

The primary use case for bcrypt is hashing passwords securely. Here’s an example:

import bcrypt

password = b"my_super_secure_password"
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())

print(f"Hashed Password: {hashed_password.decode()}")

Breaking Down the Code

  1. Input Password: The password is provided as a byte string (b prefix).
  2. Hashing: hashpw hashes the password using a salt generated by gensalt.
  3. Output: The hashed password is printed in a human-readable format.

Verifying Passwords

To authenticate a user, you can verify the provided password against the stored hash:

def verify_password(stored_hash, password):
    return bcrypt.checkpw(password.encode(), stored_hash)

# Example usage
hashed = bcrypt.hashpw(b"example_password", bcrypt.gensalt())
print(verify_password(hashed, "example_password"))  # Output: True
print(verify_password(hashed, "wrong_password"))    # Output: False

Explanation

  • checkpw: This function verifies a plaintext password against the stored hash. It returns True if the password matches the hash, otherwise False.

Configuring Salt Complexity

The gensalt function accepts a cost parameter that controls the complexity of the hash. The higher the cost, the more computationally expensive it is to generate and verify the hash.

# Using a higher cost for more computational complexity
high_cost_salt = bcrypt.gensalt(rounds=14)
high_cost_hash = bcrypt.hashpw(b"high_cost_password", high_cost_salt)

print(f"High Cost Hash: {high_cost_hash.decode()}")

Explanation

  • rounds: The number of computational rounds to perform, which must be between 4 and 31. The default is 12.

Using Environment-Specific Random Number Generation

On some systems, it may be desirable to use a cryptographic random number generator specific to the environment. Python’s bcrypt allows you to pass in a custom function for random number generation:

import os

# Custom function to generate cryptographic random bytes
def custom_random(size):
    return os.urandom(size)

# Generate a custom salt using the custom random function
custom_salt = bcrypt.gensalt(12, custom_random)
custom_hash = bcrypt.hashpw(b"custom_random_password", custom_salt)

print(f"Custom Random Hash: {custom_hash.decode()}")

Explanation

  • custom_random: Generates random bytes using os.urandom.
  • gensalt: Accepts the custom function and uses it to generate the salt.

Bcrypt Utilities

The bcrypt library also provides some utility functions to work with hashes directly.

Parsing a Bcrypt Hash

The bcrypt library doesn’t offer a direct way to parse hashes, but its structure is standardized as $2b$cost$salt$hash. Here’s an example of parsing:

import re

hash_pattern = r'\$2[abxy]\$(\d{2})\$(.{22})(.+)'
hashed = bcrypt.hashpw(b"my_password", bcrypt.gensalt())

# Extract the components using regex
match = re.match(hash_pattern, hashed.decode())
if match:
    cost, salt, hash_value = match.groups()
    print(f"Cost: {cost}, Salt: {salt}, Hash: {hash_value}")

Explanation

  • Regex Pattern: Matches the structure of a bcrypt hash string.
  • Groups: Extracts the cost, salt, and hash value from the parsed string.

Best Practices for Password Hashing

  • Never store passwords in plain text: Always hash passwords before storage.
  • Use a high work factor: Adjust the cost parameter to make hashing more computationally expensive.
  • Rehash periodically: Periodically increase the work factor and rehash stored passwords.
  • Protect salts: Ensure salts are unique and not predictable.