When building an application that validates user passwords or needs to store tokens for future use it is critical to not store these values anywhere in clear text. If there is a security breach you want to be confident that your userβs data is protected. Hashing and Encryption are a couple of techniques which can be used to achieve this and we will take a look at how to implement these with Python.
Hashing
If your application needs to allow users to register an account and create a password then you need to store the values they singed up with in order to authenticate them later. Rather than storing the passwords in clear text, this is where a hashing algorithm should be used. Hashing algorithms are one-way functions which produce the same result for the input data but given the output data are nearly impossible to reverse. There are many types of hash algorithms but SHA-256 is a strong and NIST Approved modern algorithm that fits the need of most applications in terms of strength and performance.
Create a simple Python script file to take an input and generate the SHA-256 hash with the hashlib standard library.
πΒ hash.py
import hashlib
password = input("Password: ")
password_hash = hashlib.sha256(password.encode("utf-8")).hexdigest()
print(f"Password Hash: {password_hash}")
Run the script and give it a few password inputs.
$ python3 hash.py
Password: test123
Password Hash: ecd71870d1963316a97e3ac3408c9835ad8cf0f3c1bc703527c30265534f75ae
$ python3 hash.py
Password: test123
Password Hash: ecd71870d1963316a97e3ac3408c9835ad8cf0f3c1bc703527c30265534f75ae
$ python3 hash.py
Password: test1234
Password Hash: 937e8d5fbb48bd4949536cd65b8d35c426b80d2f830c5c308e2cdec422ae2244
We can see that the same input produces the same hash result but any change such as an additional character completely changes it. The resulting hash value is what you should store in your database to later validate the userβs password.
Encryption
Hashing is a great option when you do not need use the password value. For use cases where you need to use the actual password value such as storing a long term access token to authenticate on the userβs behalf to an external application then encryption is the best option. Encryption allows you to store the values securely and decrypt them in memory when you need to use them. Python has the cryptography library which includes Fernet Symmetric Encryption to achieve this. Symmetric encryption means we will have a secret key which we can store in our environment variables and use to decrypt stored values.
Install the cryptography
and dotenv
library.
$ poetry add cryptography python-dotenv
Using version ^41.0.5 for cryptography
Using version ^1.0.0 for python-dotenv
Updating dependencies
Resolving dependencies... (0.1s)
Package operations: 4 installs, 0 updates, 0 removals
β’ Installing pycparser (2.21)
β’ Installing cffi (1.16.0)
β’ Installing cryptography (41.0.5)
β’ Installing python-dotenv (1.0.0)
Run an inline python command to generate the Fernet secret key.
$ python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key())"
b'XvYvP_c4gBDLCLbjgz6Hc47ND_BcoMYt3Cz5pAKx1qQ='
Add this value to a .env
file.
πΒ .env
SECRET_KEY=XvYvP_c4gBDLCLbjgz6Hc47ND_BcoMYt3Cz5pAKx1qQ=
Create a new python script which will load our secret key from the environment variables, instantiate the Fernet client with the key, and allow a new password to be encrypted and stored in a simple text file or print out the decrypted value of an existing stored password.
πΒ encrypt.py
import os
import sys
from cryptography.fernet import Fernet
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv("SECRET_KEY")
assert SECRET_KEY
FERNET = Fernet(SECRET_KEY)
if len(sys.argv) > 1 and sys.argv[1] == "decrypt":
with open("pw.txt") as f:
stored_password = f.read()
stored_dec_password = FERNET.decrypt(stored_password).decode()
print(f"Decrypted Password: {stored_dec_password}")
else:
new_password = input("New Password: ")
new_enc_password = FERNET.encrypt(new_password.encode()).decode()
with open("pw.txt", "w") as f:
f.write(new_enc_password)
print(f"Encrypted Password Stored: {new_enc_password}")
Test it out to validate it is working as expected.
$ python3 encrypt.py
New Password: Test123!!
Encrypted Password Stored: gAAAAABlR7V0TLTZMT_ZHEoPtqbW3B9LYgohYdUNG6Lukx9M2NSLgrFN6MUZKCNPP3Hq_KuuEPpJPPqqIktUkZTBh3qenKnQAA==
$ python3 encrypt.py decrypt
Decrypted Password: Test123!!
Awesome! πΒ This shows the core concepts of encrypting/decrypting values and in a production environment rather than storing them in a simple text file you would just store and retrieve the values from a database.
I hope you have found this article helpful for building your next amazing (and secure) application! π
Top comments (5)
Nice use of sha256. We also use it in our
.env.vault
mechanism - successor to.env
files.Have you seen python-dotenv-vault - different use case than what you are doing here, but as a fellow cryptography fan, you might find it interesting intellectually:
github.com/dotenv-org/python-doten...
@dotenv Very nice, I had not seen the dotenv vault functionality, it is a separate use case but it does address a challenge we have with keeping
.env
files in-sync across our smaller teams. I love the idea of committing encrypted environment variables to the repo with a.env.vault
file and only needing to manage theDOTENV_KEY
. π Thanks for sharing this and amazing work on the Dotenv ecosystem! β€οΈGreatππππ
Should you add salt to your passwords?
Good question. In the second example, using the cryptography library to encrypt and decrypt the passwords, the secret key they generated works as the salt added to the password. Salting does provide an added layer of protection as long as you protect the .env file where you store it. I think they just wanted to simplify the example so people will use it.