Search code examples
pythonsessioncryptographysession-cookiesweb-development-server

Fernet key must be 32 url-safe base64-encoded bytes error despite correct key length


I'm working on a Python project using aiohttp and aiohttp_session with EncryptedCookieStorage for session management. I'm generating a Fernet key in a batch script and passing it to my Python application via an environment variable. However, despite the key being of the correct length (44 characters), I'm encountering the error:

ValueError: Fernet key must be 32 url-safe base64-encoded bytes.

I am experimenting with just running a power shell script to generate the key and save as an env variable. Any suggestion on best practices for this process greatly appreciated not a lot of wisdom here...

@echo off
REM Generate the Fernet key using Python and store it in a variable
for /f "delims=" %%i in ('python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode().strip())"') do set FERNET_KEY=%%i

REM Display the generated key (optional, for debugging purposes)
echo Generated Fernet Key: %FERNET_KEY%
echo Fernet Key Length: %FERNET_KEY%

REM Set the environment variable for the current session
set "FERNET_KEY=%FERNET_KEY%"

REM Run your Python application with the environment variable set
cmd /c "set FERNET_KEY=%FERNET_KEY% && python main.py"

And this is a snip of the aiohttp app:

import os
import logging
from aiohttp import web
import aiohttp_cors
import aiohttp_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage
import hashlib
from cryptography.fernet import Fernet

# Set up logging
logging.basicConfig(level=logging.DEBUG)

# Retrieve Fernet key from environment variables
FERNET_KEY = os.getenv("FERNET_KEY")
if not FERNET_KEY:
    raise ValueError("FERNET_KEY environment variable not set")

FERNET_KEY = FERNET_KEY.strip()
logging.debug(f"FERNET_KEY from environment: '{FERNET_KEY}'")
logging.debug(f"FERNET_KEY length: {len(FERNET_KEY)}")

if len(FERNET_KEY) != 44:
    raise ValueError("FERNET_KEY must be 32 url-safe base64-encoded bytes (44 characters long)")

# Validate and encode the Fernet key
fernet_key = FERNET_KEY.encode()
logging.debug(f"Encoded Fernet Key: {fernet_key}")

# Retrieve admin credentials from environment variables
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "password")
USERS = {ADMIN_USERNAME: hashlib.sha256(ADMIN_PASSWORD.encode()).hexdigest()}

and error I cannot seem to rectify:

Generated Fernet Key: oGpGY-WldRjBpAzBMr3AfBQdKqIOZ530KegvDbsXMuk=
Fernet Key Length: oGpGY-WldRjBpAzBMr3AfBQdKqIOZ530KegvDbsXMuk=
DEBUG:root:FERNET_KEY from environment: 'oGpGY-WldRjBpAzBMr3AfBQdKqIOZ530KegvDbsXMuk='
DEBUG:root:FERNET_KEY length: 44
DEBUG:root:Encoded Fernet Key: b'oGpGY-WldRjBpAzBMr3AfBQdKqIOZ530KegvDbsXMuk='
Traceback (most recent call last):
  File "C:\Users\bbartling\Desktop\react-login\server\main.py", line 114, in <module>
    aiohttp_session.setup(app, EncryptedCookieStorage(fernet_key))
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\site-packages\aiohttp_session\cookie_storage.py", line 47, in __init__
    self._fernet = fernet.Fernet(secret_key)
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\site-packages\cryptography\fernet.py", line 40, in __init__
    raise ValueError: Fernet key must be 32 url-safe base64-encoded bytes.

I've verified that the key is 44 characters long, but it still throws this error. The debug logs show the key length and the exact value being used.

The Fernet key is being used for session-based authentication in a front-end application made in React. Any tips appreciated for best practices on creating a secure login for a aio-http and React app.


Solution

  • From the source code for EncryptedCookieStorage:

    class EncryptedCookieStorage(AbstractStorage):
        """Encrypted JSON storage."""
    
        def __init__(
            self,
            secret_key: Union[str, bytes, bytearray, fernet.Fernet],
            #  ... lots of other arguments ...
        ) -> None:
            #
            # ... other code ...
            #
            if isinstance(secret_key, fernet.Fernet):
                self._fernet = secret_key
            else:
                if isinstance(secret_key, (bytes, bytearray)):
                    secret_key = base64.urlsafe_b64encode(secret_key)
                self._fernet = fernet.Fernet(secret_key)
    

    we see that the type of the secret_key argument is examined to determine how to treat it. If it's a bytes or bytearray object it assumes it is already base64url decoded to it base64url encodes it first. If it's a 'str' then it's treated like it's base64url encoded. So, don't do this

    fernet_key = FERNET_KEY.encode()
    

    and instead just do

    fernet_key = FERNET_KEY
    

    Or you can supply a bytes object if you base64url decode it first, or you can supply an already constructed Fernet object.