1

According to the documentation for the cryptography.fernet module, fernet keys are:

A URL-safe base64-encoded 32-byte key

Yet this doesn't work:

import secrets
from cryptography import fernet

f = fernet.Fernet(secrets.token_urlsafe(32))

failing with ValueError: Fernet key must be 32 url-safe base64-encoded bytes - however the documentation for token_urlsafe claims that it returns

a random URL-safe text string, containing nbytes random bytes. The text is Base64 encoded ...

Likewise, this doesn't work:

import base64
from cryptography import fernet

key = fernet.Fernet.generate_key()
print(base64.b64decode(key))

failing with: binascii.Error: Incorrect padding.

So what is a Fernet key and what is the right way to go about generating one from a pre-shared string?

Tom
  • 7,269
  • 1
  • 42
  • 69
  • https://github.com/fernet/spec/blob/master/Spec.md – President James K. Polk May 02 '23 at 16:06
  • `secrets.token_urlsafe` doesn't pad the base64 encoding so it is not considered valid base64 by `base64.urlsafe_b64decode`. Similarly, a `urlsafe_b64encode` value will fail to be decoded with `b64decode`. So, as documented, Fernet requires 32 bytes that are decodable using the `base64.urlsafe_b64decode` function. Generally the simplest way to achieve this would be to call `base64.urlsafe_b64encode` on your initial bytes. – Paul Kehrer May 06 '23 at 18:27

1 Answers1

-1

FWIW, it took ChatGPT a lot of attempts (and me telling it how to do it) to get this right. To generate a key from a pre-shared string:

key = "very sekrit key"
key += '=' * (32 - len(key))
key = base64.urlsafe_encode(key.encode("utf-8"))

This is not a good approach; Fernet uses the first 16 bytes of the key for signing and the second 16 bytes for encryption, so the above example ends up with an encryption key of "================".

The issue with secrets.token_urlsafe(32) is that, although it does produce a URL-safe base-64-encoded string, the result is not correctly padded for base-64 decoding. This can be rectified like this:

key = secrets.token_urlsafe(32)
key += '=' * (4 - len(key)%4)
cryptor = fernet.Fernet(key)

In this case, the padding is benign because it's not actually added to the key content, just to the base64-encoded key.

A better approach to generating a key from a pre-shared string is to use a key derivation function such as those in the kdf package.

Tom
  • 7,269
  • 1
  • 42
  • 69
  • Password based KDFs exist for this exact purpose (Scrypt, argon2id, etc). When simply using a hash function an attacker can rapidly guess keys from password strings, whereas a PBKDF forces an expensive computational operation. The Fernet docs themselves contain an example of using a PBKDF (https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet) although Scrypt is generally a better choice than PBKDF2. – Paul Kehrer May 06 '23 at 18:24
  • @PaulKehrer - I've updated the answer to point to KDFs rather than using a hash function. However, could you clarify why you think Scrypt is a better choice for this particular case? This is purely key stretching and the resulting key isn't stored anywhere, making the cost of the KDF less important (AFAICT - I'm no expert here). – Tom May 08 '23 at 10:05
  • Passwords are low entropy strings, which means attackers can and will simply try giant dictionaries of them from password breaches, etc. Accordingly, you want to make the derived key expensive to create so that an attacker can't rapidly test keys. This is what PBKDFs provide. Scrypt and argon2id have an additional design goal of making it expensive in both time (CPU) and space (memory), which makes building dedicated hardware to accelerate the KDF much more expensive. – Paul Kehrer May 10 '23 at 03:33