# How the Open Notebook Encryption Key System Uses Fernet for Credential Storage

> Learn how Open Notebook secures API keys at rest using Fernet symmetric encryption. Discover how it derives keys from environment variables for robust credential storage.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-24

---

**Open Notebook uses Fernet symmetric encryption (AES-128-CBC with HMAC-SHA256) to automatically encrypt API keys at rest, deriving the encryption key from a user-supplied environment variable hashed with SHA-256.**

The `lfnovo/open-notebook` repository implements a transparent field-level encryption system that protects provider API credentials using Python's `cryptography` library. By leveraging Fernet's authenticated symmetric encryption, the system ensures that sensitive data remains encrypted in the database while requiring minimal changes to application logic.

## Fernet Key Derivation and Management

The encryption system centers on [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), which handles the transformation of user-supplied secrets into valid Fernet keys and provides encrypt/decrypt utilities.

### Deriving Keys from Environment Variables

The system expects a master secret via the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable (or its Docker secret counterpart). When the application first requires encryption services, the `_get_encryption_key` function (lines 78-84) lazily loads this value and passes it to `_ensure_fernet_key` (lines 96-103).

The `_ensure_fernet_key` function processes the arbitrary string through SHA-256 hashing, then Base64-url-encodes the 32-byte digest to produce a specification-compliant Fernet key (lines 104-112). This derived key then initializes a `Fernet` instance via `get_fernet()` (lines 15-25).

This approach allows administrators to use any passphrase while ensuring the underlying cryptographic key meets Fernet's strict 32-byte requirement.

### Core Encryption Utilities

The module exposes two primary functions for credential protection:

- **`encrypt_value`** (lines 28-42): Accepts a plaintext string, encodes it to UTF-8 bytes, applies `Fernet.encrypt`, and returns a Base64-encoded token.
- **`decrypt_value`** (lines 66-80): Attempts to decrypt Fernet tokens using `Fernet.decrypt`. If decryption fails but the token resembles a Fernet format, it raises a clear error; otherwise, it returns the raw value unchanged to support legacy plaintext credentials (lines 85-96).

## Automatic Credential Encryption in the Domain Layer

The `Credential` domain model in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) integrates encryption seamlessly into the persistence lifecycle, ensuring `api_key` fields never reach the database in plaintext.

### Encryption on Save

When a `Credential` instance is persisted via `_prepare_save_data` (lines 34-40), the system extracts the underlying string from the `api_key` field (stored as Pydantic's `SecretStr`). This value passes to `encrypt_value`, producing an opaque token that replaces the plaintext in the database row.

### Decryption on Load

During retrieval operations such as `Credential.get`, `Credential.get_all`, or `_from_db_row`, the stored token undergoes decryption via `decrypt_value`. The recovered plaintext is immediately wrapped back in `SecretStr` (lines 60-68) before returning to application code, maintaining type safety and preventing accidental logging of secrets.

## Practical Implementation Examples

Configure the encryption key via environment variable:

```python

# .env or Docker environment

# OPEN_NOTEBOOK_ENCRYPTION_KEY=my-super-secret-passphrase

```

Manual encryption and decryption for debugging or external tooling:

```python
from open_notebook.utils.encryption import encrypt_value, decrypt_value

plain = "sk-abc123"
cipher = encrypt_value(plain)      # Returns Fernet token

recovered = decrypt_value(cipher)   # Returns "sk-abc123"

```

High-level credential API handling encryption automatically:

```python
from open_notebook.domain.credential import Credential
from pydantic import SecretStr

# Create and save (automatically encrypts)

cred = Credential(
    name="Production",
    provider="openai",
    modalities=["language"],
    api_key=SecretStr("sk-prod-key")
)
await cred.save()

# Retrieve and use (automatically decrypts)

saved = await Credential.get(cred.id)
print(saved.api_key.get_secret_value())  # "sk-prod-key"

```

## Summary

- **Fernet symmetric encryption**: Open Notebook uses AES-128-CBC with HMAC-SHA256 authentication via the `cryptography` library's Fernet implementation.
- **SHA-256 key derivation**: The `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable is hashed and Base64-encoded in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) to generate a valid 32-byte Fernet key.
- **Transparent field-level encryption**: The `Credential` class in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) automatically encrypts `api_key` fields before saving and decrypts them upon retrieval.
- **Legacy support**: The `decrypt_value` function gracefully handles legacy plaintext values while strictly validating Fernet tokens.
- **Type safety**: Decrypted values are wrapped in Pydantic's `SecretStr` to prevent accidental exposure in logs or traces.

## Frequently Asked Questions

### What cryptographic algorithm does the Open Notebook encryption system use?

The system uses **Fernet**, which combines **AES-128-CBC** encryption with **HMAC-SHA256** authentication. This provides authenticated symmetric encryption, ensuring both confidentiality and integrity of stored API keys.

### How do I configure the encryption key in production environments?

Set the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable to a strong, unique passphrase before starting the application. In Docker deployments, you can use Docker secrets mounted at `/run/secrets/OPEN_NOTEBOOK_ENCRYPTION_KEY`. The system hashes this value with SHA-256 and Base64-encodes it to derive the actual Fernet key, meaning you can use any string without worrying about specific key length requirements.

### What happens if I lose or change the encryption key?

If you lose the `OPEN_NOTEBOOK_ENCRYPTION_KEY`, you cannot decrypt existing credentials stored in the database. The system does not store the key locally; it relies entirely on the environment variable. Changing the key will prevent decryption of existing encrypted values, effectively rendering stored API keys inaccessible. You would need to re-create credentials with the new key.

### Can existing plaintext credentials be migrated to encrypted storage?

Yes. The `decrypt_value` function in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) (lines 85-96) includes backward compatibility logic that returns raw values unchanged if they do not match the Fernet token format. When you save an existing credential retrieved in plaintext mode, the `_prepare_save_data` method automatically encrypts it, migrating the value to secure storage on the next write operation.