Home Articles Categories Series
Pythonise Just now

Password hashing in Python with Bcrypt

Using the bcrypt library to securely hash and check hashed passwords with Python


Article Posted on by in Python
Julian Nash 8 months ago in Python

The security of users and user data should always be a priority any developer, especially when it comes to personal information such as passwords.

Stroring passwords as plain text, as we all know is extremely dangerous and just plain silly. If a database gets breached and plain text passwords are leaked, it's game over.

In this article, we're going to be exploring password hashing with bcrypt, a popular, well tested and secure hashing library/algorithm available for Python.

Note - This isn't going to be a detailed guide on the inner working of hashing, more of a high level overview to introduce you to some of the concepts and best practices.

What is hashing

Not to be confused with encryption, hashing is the irriversable, one way process of taking a string and turning into a fixed length of seemingly random characters.

For example, here's the word encyclopedia hashed using bcrypt:

b'$2b$12$D0mO1kpoNj2gCMvKsps9i.6BMbrcJUCGDP/h0awUQ6C77.nY7gUVm'

Unlike encryption which can be decoded and reversed into its original form, hashing is irriversable, meaning there's no way to retrieve the original string, making it ideal for passwords and authentication.

Comparing passwords

If we run the same bcrypt hashing function again on the same word encyclopedia, we get:

b'$2b$12$emh2PDMY.r42ceV3X5PtFOGA52/ESfDsCAQ.Muj7biO2Rqc8DFdjS'

Which is clearly quite different from our first hash... 馃

If you're not familiar with hashing, you may be thinking at this point "So how do I compare a password with a hashed password?, especially when they're totally different"... and that's a great question.

bcrypt comes with a function to allow us to check an unhashed string against a previously hashed string, meaning we can do things like logging a user into their account based on a plain text password they provide if it matches their hashed password stored in a database.

Cracking passwords

Hashed passwords can be cracked, quite easily really.

Here's a link to a great video featuring Dr Mike Pound over at Nottingham university explaining and demonstrating some of the techniques for cracking hashed passwords - https://www.youtube.com/watch?v=7U-RbOKanYs

Dr Pound will do a better job at explaining password cracking than I, but to summarize some of the techiques used:

Brute force attacks

Brute force attacks work by trying every different combination of characters for a given length, for example:

# 5 character length
a
aa
aaaa
aaaaa
b
ab
aaab
aaaab
c
ac
aac
aaac
...
zzzzz

Until every possible combination of 5 characters have been hashed and compared.

However most modern cracking algorithms have moved onto something more advanced.

Dictionary attacks

A dictionary attack is the process of taking a list of words (Previously cracked passwords, commonly used words, randomly generated words etc..), hashing and then comparing them to the hashed passwords.

The dictionary cracking algorithms will try dirrerent combinations of words which are much more likely to be a real password as many of the word lists include actual passwords, typically aquired from big database breaches.

For example, theres a password list called "Rock you" containing somewhere around 14 million leaked passwords.

Just to put the speed of these cracking algorithms into perspective, a good quality graphics card can hash and compare somewhere around 4 - 10 billion words per second... PER SECOND!

The good news is that the chances of this can be dramatically reduced by following some best practices:

  • Securing your database
  • Using a reputable hashing function
  • Requiring users to provide longer passwords (At least 9 characters, ideally more)
  • Requiring a mixture of uppercase, lowercase, numbers and non aplhanumerical characters
  • Using uncommon words in a seemingly random order

The obvious one here is not letting your database fall into the hands of malicious actors, followed by requiring your users to create passwords of a suitable length, non alphanumeric characters and random uncommon words.

Installing bcrypt

Installing bcrypt is a simple pip install away:

pip install bcrypt

We're going to create a virtual environment and install bcrypt with pip:

python -m venv env
source env/bin/activate
pip install bcrypt

Hashing passwords

To use bcrypt, you'll need to import it:

import bcrypt

Hashing passwords or any other string is incredibly simple using the bcrypt.hashpw() function.

bcrypt.hashpw() takes 2 arguments:

  • A string (bytes)
  • Salt

Salt is random data used in the hashing function and the randomness of it is important. We're not going to cover salt in this article but feel free to read this Wikipedia article for more information.

Fortunately for us, bcrypt also provides a function to generate salt for us - bcrypt.gensalt().

Let's hash a password and print it:

password = b"SuperSercet34"

hashed = bcrypt.hashpw(password, bcrypt.gensalt())

print(hashed)

This snippet prints:

b'$2b$12$PI52AjIXvX/Y68h1dIjpbekSm/bdk7wt0nz/Lwo7yD9tHQaYmb9Ga'

And took 0.19132542610168457 ms (which will become more relevant later)

We used the b prefix on the password string to create a byte string, however if you were taking input from a user, you may want to call the .encode("utf-8") method on the string:

password = "SuperSercet34".encode("utf-8")

# Get a password from a form using Flask and encode it before hashing
password = request.form.get("password").encode("utf-8")

Checking passwords

bcrypt also comes with a function to check plain text passwords against hashed passwords, returning True if the passwords match, else returning False.

bcrypt.checkpw() takes 2 arguments:

  • The plain text password (Must be bytes)
  • The hashed password

Let's hash a password and check it:

password = b"SuperSercet34"

hashed = bcrypt.hashpw(password, bcrypt.gensalt())

# Check if password matches the hashed password
if bcrypt.checkpw(password, hashed):
    print("Password match!")
    # Log the user in ...
else:
    print("Password didn't match")
    flash("Invalid credentials", "warning")

As expected, we got Password match!.

Again, if you're taking user data (On a login page for example), you'll need to call .encode("utf-8") on the user input password before checking it using bcrypt.checkpw()

Work factor

The "work factor" of a cryptographic system is the amount of time and resource required to break the system or its process.

bcrypt features an adjustable work factor which we can pass to bcrypt.gensalt() using the rounds argument and providing an integer (The default is 12).

Note - Whilst adjusting the work factor will make your passwords slower to crack, it will also consume resources on your machine.

Another important note - Do not set the rounds argument too high! Your system may hang or just take too long to return a response.

For reference, we'll run bcrypt.hashpw() and pass a few different values as rounds to bcrypt.gensalt() and time the results:

  • No argument (Default) - 0.192 ms:
password = "SuperSercet34".encode("utf-8")

start = time.time()
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
end = time.time()

t = end - start
print(t)
  • Work factor of 14 - 0.755 ms:
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=14))
  • Work factor of 15 - 1.522 ms:
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=15))
  • Work factor of 16 - 3.018 ms:
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=15))
  • Work factor of 18 - 12.258 ms:
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=15))

As you'll see, rounds=15 takes considerably long to hash, so use wisely.

Consider thinking about what might be an acceptable time for your user to wait whilst you hash their password and adjust the work factor accordingly, also being sure to consider your server performance/hardware specs.

Wrapping up

Whilst this article wasn't designed to be an in depth guide on hashing algorithms, I hope it's shown you how easy it can be to hash and check passwords with Python using bcrypt, along with pointing out some of the dangers of not hashing passwords or enforcing users to provide a complex password.

Happy hashing!

Last modified 14 Mar 2019
Did you find this article useful?
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
Contents
Loading...