# Password hashing

## Password hashing in Python with Bcrypt

Using the bcrypt library to securely hash and check hashed passwords with 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 <a href="#what-is-hashing" id="what-is-hashing"></a>

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`:

```python
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 <a href="#comparing-passwords" id="comparing-passwords"></a>

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

```python
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 <a href="#cracking-passwords" id="cracking-passwords"></a>

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](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 <a href="#installing-bcrypt" id="installing-bcrypt"></a>

Installing bcrypt is a simple `pip` install away:

```python
pip install bcrypt
```

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

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

#### Hashing passwords <a href="#hashing-passwords" id="hashing-passwords"></a>

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

```python
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](https://en.wikipedia.org/wiki/Salt_\(cryptography\)) 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:

```python
password = b"SuperSercet34"

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

print(hashed)
```

This snippet prints:

```python
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:

```python
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 <a href="#checking-passwords" id="checking-passwords"></a>

`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:

```python
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 <a href="#work-factor" id="work-factor"></a>

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:

```python
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:

```python
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=14))
```

* Work factor of 15 - `1.522` ms:

```python
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=15))
```

* Work factor of 16 - `3.018` ms:

```python
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=15))
```

* Work factor of 18 - `12.258` ms:

```python
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 <a href="#wrapping-up" id="wrapping-up"></a>

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\
Reference : [https://pythonise.com/categories/python/python-password-hashing-bcrypt](https://pythonise.com/categories/python/python-password-hashing-bcrypt)
