Fernet system for symmetric encryption
Categories: cryptography

This article is part of a series on the Python cryptography library.
Refer to the glossary of cryptography terms for definitions of any terms used in this chapter.
Overview of Fernet
Fernet is a system for symmetric encryption/decryption, using current best practices. It also authenticates the message, which measm that the recipient can tell if the message has been altered in any way from what was originally sent.
Fernet overcomes many of the problems obvious mistakes a naive developer might make when designing such a system by:
- Providing a secure mechanism for generating keys (a key is similar to a password).
- Selection a secure encryption algorithm (AES using CBS mode and PKCS7 padding)
- Randomly allocating a secure "salt" value IV) to make the encryption more secure.
- Timestamping the encrypted message.
- Signing the message (using HMAC and SHA256) to detect any attempts to change it.
We will look at the hows and whys of these features later in this article.
Using Fernet
Fernet is included in the cryptography
library.
To encrypt and decrypt data, we will need a secret key that must be shared between anyone who needs to encrypt or decrypt data. It must be kept secret from anyone else, because anyone who knows the key can read and create encrypted messages. This means we will need a secure mechanism to share the key. The same key can used multiple times.
Creating a key
We can create a key like this:
from cryptography.fernet import Fernet
import base64
key = Fernet.generate_key()
The key is a random value, and will be completely different each time you call the generate_key
function.
Encrypting a message
To encrypt a message, we must first create a Fernet
object using the key created previously. We than call the encrypt
function, passing the data we wish to encrypt is the form of a bytes
array:
cipher = Fernet(key)
message = "Message to be encrypted".encode('utf-8')
token = cipher.encrypt(message)
Notice that we use the encode('uft-8')
method to convert our message string into a bytes array. If we want to encrypt an image or other data, we must load it into memory as a byte array.
The encrypted message is stored in token
in the format described below.
Decrypting a message
To decrypt a message, we must again create a Fernet
object using the same key that was used to encrypt the data. We than call the decrypt
function, passing the data we wish to decrypt is the form of a bytes
array. The function returns the decrypted original message.
cipher = Fernet(key)
decoded = cipher.decrypt(token)
decrypt
will raise an exception if it cannot decode token
for any reason. For example:
- The token is malformed, most likely because it has an invalid length, see later.
- The HMAC signature doesn't match. This could because the key is incorrect or because the token has been modified after creation.
- The token has expired.
Fernet tokens contain a timestamp which allows testing for an expired message. To do this you must add the a ttl
(time to live) parameter to the decrypt
function that specifies a maximum age (in seconds) of the token before it will be rejected. For example:
decoded = cipher.decrypt(token, 24*60*60)
This will reject any messages that are more than 1 day old. If you do not set a ttl
value (or set it to None
), the age of the token will not be checked at all.
Fernet key and token format
A fernet key as returned by the generate_key
actually contains two 16-byte keys:
- A signing key used to sign the HMAC.
- A private key used by the encryption.
These two values are concatenated to form a 32 byte value:
This 32 byte key is then encoded using Base64 encoding. This encodes the binary quantity as string of ASCII characters. The variant of Base64 used is URL and filename safe, meaning that it doesn't contain any characters that aren't permitted in a URL or a valid filename in any major operating system.
If we print the key, the result is a 44 byte string representing the 32 byte bunary value.
print(key) # b'K7GVACyA63l--mNkBjQ5tbkDxO6yCJkmr9D-uV5T-wU='
A Fernet token contains the following fields:
The fields are:
- Version, 1 byte - the only valid value currently is 128.
- Timestamp 8 bytes - a 64 bit, unsigned, big-endian integer that indicates when the ciphertext was created. Time is represented as the number of seconds since the start of Jan 1, 1970, UTC.
- IV 32 bytes - the 128 bit Initialization Vector used in AES encryption and decryption.
- Ciphertext - the encrypted version of the plaintext message. This is encrypted using AES, in CBC mode, using the encryption key section of the Fernet key. The ciphertext is padded to be a multiple of 128 bits, which is the AES block size, using the PKCS7 padding algorithm. This menas that the ciphertest will always be a multiple of 16 bytes in length, but the padding will be automatically removed when the data is decrypted.
- HMAC - a 256-bit HMAC of the concatenated Version, Timestamp, IV, and Ciphertext fields. The HMAC is signed using the signing key section o fteh Fernet key.
The entire token (including the HMAC) is encoded using Base64. The HMAC is calculated using the binary data of the Version, Timestamp, IV, and Ciphertext fields, before Base64 encoding is applied.
Join the PythonInformer Newsletter
Sign up using this form to receive an email when new content is added:
Popular tags
2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest