DEV Community

Cover image for Python - Stop storing passwords in plain text! A guide to werkzeug.security

Python - Stop storing passwords in plain text! A guide to werkzeug.security

Problem

Storing passwords in plain text presents numerous dangers.

  • What happens if your database is compromised?
  • What if you accidentally reveal the wrong window while demonstrating and sharing your screen?

Such mistakes can result in serious issues, including losing the trust of customers, damaging brand value, financial repercussions, legal penalties, etc. These kinds of incidents are not rare; news reports about data breaches frequently appear worldwide.

Refer to https://databreaches.net/ for more details around data breaches worldwide.

Therefore, we should consider security in each aspect of application development, starting from planning, coding, building, deployment, etc.

And in this article, we will be talking about hashing passwords, but how do we actually do that? What options are available, etc.?

  • There are numerous options available, out of which some of the famous ones are werkzeug.security, argon2, bcrypt, etc.
  • Here we will see what werkzeug.security is and its practical implementation.

Prerequisites

  • Working Python setup.
  • Werkzeug Library installed.
pip install werkzeug
Enter fullscreen mode Exit fullscreen mode

Basics

How does this work?
werkzeug.security provides helper functions for secure password storage and verification, and the two most commonly used functions are:

  • generate_password_hash(password, method=XXX, salt_length=XXX)
  • check_password_hash(PASSWORD_HASH, ACTUAL_PASSWORD)

More details around this can be found in its official documentation, refer: https://werkzeug.palletsprojects.com/en/stable/utils/ for more details.

The basic principle behind it is,

  1. generate_password_hash takes the actual plain-text password as an input.
  2. Further generates a salt of specified length.
  3. The password + salt is processed through the chosen algorithm.
    • scrypt(default)
    • pbkdf2
  4. Finally, it generates the output as a structured, hashed string.
  5. Then check_password_hash comes into the picture, it securely checks if the given password and the stored password hash match or not.
  6. Once that is evaluated, it produces a Boolean result, either true or false.

HOWTO

generate_password_hash

# Import generate_password_hash
from werkzeug.security import generate_password_hash

# Plain text password, if using Flask, it can come from any web form
plain_password = "SHH_SECRET"

# Generate a hashed string using default parameters
hashed = generate_password_hash(plain_password)

# Printing a hashed string
print("Hashed String: ", hashed)
Enter fullscreen mode Exit fullscreen mode
# Output
Hashed String:  scrypt:32768:8:1$A2qi9GPp8Ut3cESv$31e31ffc4d21251396b391c31f09f6637e9169d4c5541a790cf0d9fcae3d58d2ba8a2df51e13f2601ba77fabe2e68d517fe845c63b489418309791ae1e350d3b
Enter fullscreen mode Exit fullscreen mode

If you observe, the hashed string contains multiple sections
scrypt:N:r:p(METHOD_and_PARAMETERS)$salt(RANDOM_SALT)$derived_hash(ACTUAL_HASHED_PASSWORD)

In the above example, we just used the default values to generate the password hash.

Method: scrypt
N: 32768
r: 8
p: 1
Enter fullscreen mode Exit fullscreen mode

Quick note about the parameters: Based on my understanding, the parameters N, r, and p in scrypt directly control how much compute and memory are required to derive a single password hash. The larger the numbers, the harder and more computationally intensive it is to break the password.

If you want to use custom parameters, this is how we can use them:

generate_password_hash(plain_password, method="scrypt:32768:16:1", salt_length=32)
Enter fullscreen mode Exit fullscreen mode

It will produce the output as:

scrypt:32768:16:1$0hkax1LolbhqTyEN9rMSi8MC2MoF5B8w$85a7175deab3795db17898d17f03e3164df0ae1d158081ca3ed1680f9f940ff9cac76df18a71595160fd39963f4b4811c82348354e0c4a67a328104c56900230
Enter fullscreen mode Exit fullscreen mode

if you want to use another method, this is how we can use it:

generate_password_hash(plain_password, method="pbkdf2:sha256")
Enter fullscreen mode Exit fullscreen mode

The execution of generate_password_hash function generates a string that needs to be stored and checked for verification.

check_password_hash

# Import generate_password_hash and check_password_hash
from werkzeug.security import generate_password_hash, check_password_hash

# Plain text password, if using Flask, it can come from any web form
plain_password = "SHH_SECRET"
# Generate a hashed string using default parameters
hashed = generate_password_hash(plain_password)

# Login attempt 1 with wrong password
# If using Flask, it can come from any web form, like a login screen
login_attempt_1 = "SHH_WRONG_SECRET"
is_valid = check_password_hash(hashed, login_attempt_1)
print("Password correct?", is_valid)

# Login attempt 2 with correct password
# If using Flask, it can come from any web form, like a login screen
login_attempt_2 = "SHH_SECRET"
is_valid = check_password_hash(hashed, login_attempt_2)
print("Password correct?", is_valid)
Enter fullscreen mode Exit fullscreen mode
# Output
# This is the first attempt in which we used the wrong password, and it concluded that it was incorrect, hence it gave the output as false
Password correct? False
# This is the second attempt in which we used the correct password, and it concluded that it was correct, hence it gave the output as true
Password correct? True
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • Never store plaintext passwords; store only the hash strings.
  • Do not re-invent the wheel; we can easily rely on vetted libraries and generate hash strings.
  • Keep your dependencies up-to-date.
  • Don’t compare strings manually; we can easily leverage functions like check_password_hash, which handle secure comparison.

Soon, I will be adding details about other libraries and functions as well. I hope this will be helpful.

Feel free to add your thoughts and experiences. Also, since I am still learning, correct me if I am wrong or have misinterpreted anything. Happy Learning! 😄

Top comments (0)