Encrypting data in python with fernet

Page contents

Encrypting files is a common need for modern software, and python meets this requirement with the fernet encryption suite, located in the cryptography standard library module. Fernet is a symmetric encryption suite using AES-128-CBC and HMAC for authentication. Fernet is incredibly easy to use even for developers with no experience in cryptography, while offering strong security up to modern standards. *Be warned though: it is a python-specific format - if you need interoperability with other languages/systems, you need to look elsewhere for your encryption needs ...*

Simple encryption

The most basic use of fernet consists of three steps: generate a key, create a Fernet, encrypt your data. It really is that simple!


Here is an example:

from cryptography.fernet import Fernet

key = Fernet.generate_key()
suite = Fernet(key)
encrypted_message = suite.encrypt(b"my secret data")

Now encrypted_message contains the encrypted contents of the secret strings passed into suite.encrypt().


If you want to decrypt it again, call suit.decrypt() instead:

# assuming you created 'suite' with the same key used for encryption
plaintext_message = suite.decrypt(encrypted_message)

It is important to keep the key contents somewhere safe, as it is the only way to decrypt the data, and anyone in possession of it can freely decrypt it.

Encrypting with a password

Many uses of encryption need prefer to use user-supplied passwords for encryption rather than generated keys. Fernet can fit this use case as well, by using a key derivation function (KDF) to derive a suitable key from a user password.

You should ensure to use a modern KDF designed for this purpose like scrypt or argon2id which produces a cryptographically secure key of the correct length. (The documentation still shows `PBKDF2HMAC`, but you should use better functions if you can).


In this example, we use scrypt since it is also available in the cryptography module:

import os
import base64
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt

password = b"my secret password"
salt = os.urandom(16)
kdf = Scrypt(
    salt=salt,
    length=32,
    n=2**14,
    r=8,
    p=1,
)
key = base64.urlsafe_b64encode(kdf.derive(password))
suite = Fernet(key)

# assuming you loaded 'encrypted_message' previously
plaintext_message = suite.decrypt(encrypted_message)

You may notice the salt used in the password generation. This is a random value, necessary to prevent lookup attacks and ensuring different encryption passes produce different keys, even if they use the same password. Without it, an attacker could precompute keys for common passwords once and immediately test them against the encrypted data, whereas a salt forces them to always recompute the hash using the (intentionally) slow and expensive key derivation function.

Since the salt value is needed to compute the key, you must also store it somewhere and have it ready when decrypting the encrypted message. There is no need to keep the salt confidential (it's just random bytes), so you can easily store it alongside the encrypted data.

Storing the salt alongside the encrypted data

Storing the salt from the KDF alongside the encrypted data is one of the common ways to ensure you have both of them available when decrypting the data.


This process can be combined into two convenience functions

import os
import base64
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt


def _derive_key(password: bytes, salt: bytes) -> bytes:
    kdf = Scrypt(
        salt=salt,
        length=32,
        n=2**14,
        r=8,
        p=1,
    )
    return base64.urlsafe_b64encode(kdf.derive(password))


def encrypt(password: bytes, data: bytes) -> bytes:
    salt = os.urandom(16)
    key = _derive_key(password, salt)
    suite = Fernet(key)
    encrypted = suite.encrypt(data)
    return salt + encrypted


def decrypt(password: bytes, encrypted_data: bytes) -> bytes:
    salt = encrypted_data[:16]
    ciphertext = encrypted_data[16:]
    key = _derive_key(password, salt)
    suite = Fernet(key)
    return suite.decrypt(ciphertext)


if __name__ == "__main__":
    password = b"my secret password"
    message = b"Attack at dawn!"

    encrypted = encrypt(password, message)
    print(f"Encrypted: {encrypted!r}")

    decrypted = decrypt(password, encrypted)
    print(f"Decrypted: {decrypted!r}")

The encrypt() function generates a random salt value on every run and prepends it to the encrypted message before returning. Since the salt's length is always the same, the decrypt() function can easily strip the first 16 bytes from the encrypted data and use it as the salt to reproduce the correct key from the user password, then decrypt the remaining data with it.

Encrypting files

Fernet is absolutely ill-suited to encrypt large files, because it requires that the message contents fit into memory. This means that the entire data you want to encrypt, the entire output data and the overhead from the encryption process need to fit into memory at the same time, potentially using several gb of memory or more every time.


Consider the fernet_files module for file encryption, which takes care of encrypting file contents in chunks, together with a secure storage format and resistance against chunk reordering and similar attacks.

More articles

Setting up port knocking for SSH

Hiding important services from unauthorized eyes

Running local text to speech using chatterbox

Multi-language text to speech with optional voice cloning without external services

Modern backup management with restic

Reliable backups from storage to recovery