# Open Notebook Credential Encryption: How API Keys Are Protected in the Database

> Learn how Open Notebook encrypts API keys using Fernet symmetric encryption to protect your credentials in the database. See how plaintext keys are never stored.

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

---

**Open Notebook encrypts API keys using Fernet symmetric encryption (AES-128-CBC with HMAC-SHA256) before storing them in the database, ensuring plaintext credentials never touch persistent storage.**

The lfnovo/open-notebook repository implements a robust credential encryption layer to safeguard sensitive provider API keys at rest. By leveraging the Python cryptography library's Fernet implementation, the system automatically encrypts credentials before persistence and decrypts them only when loaded into application memory. This approach ensures that database breaches or unauthorized access never expose plaintext secrets.

## How Credential Encryption Works in Open Notebook

The encryption architecture centers on [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), which provides deterministic symmetric encryption using Fernet (AES-128-CBC with HMAC-SHA256 authentication). The process involves three key stages: key derivation, encryption utility functions, and automatic field-level handling in the domain model.

### Encryption Key Source and Derivation

The system reads the raw encryption secret from the environment variable `OPEN_NOTEBOOK_ENCRYPTION_KEY` (or the Docker-compatible `OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE` for file-based secrets). The helper function `_ensure_fernet_key` in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) processes this input by hashing it with SHA-256 and then base64-url-encoding the result to produce a valid 32-byte Fernet key. This design accepts any string as input while ensuring cryptographic compatibility with the Fernet specification.

### The Fernet Encryption Engine

The `get_fernet()` function constructs a `cryptography.Fernet` instance from the derived key. Two primary utilities expose this functionality to the rest of the application:

- `encrypt_value(value: str) → str`: Encrypts plaintext and returns a URL-safe base64-encoded token.
- `decrypt_value(value: str) → str`: Decrypts Fernet tokens, with fallback logic to return the original value unchanged for legacy unencrypted data.

### Automatic Encryption in the Credential Model

The `Credential` domain model in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) automatically intercepts the `api_key` field (typed as `pydantic.SecretStr`) during save operations. Before persistence, it extracts the secret value and passes it through `encrypt_value()`, storing only the resulting ciphertext in the database. Upon retrieval, the model passes the stored ciphertext through `decrypt_value()` and re-wraps the result in a `SecretStr`, keeping the decrypted key available only in volatile application memory.

## Working with Encrypted Credentials

Developers interact with encrypted credentials through the standard `Credential` model interface without manual encryption steps.

### Storing a New API Key

When creating a credential, simply wrap the API key in `SecretStr`. The encryption happens automatically during the save operation:

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

cred = Credential(
    name="Production OpenAI",
    provider="openai",
    api_key=SecretStr("sk-abcdef123456789"),
)
await cred.save()  # encrypt_value() is called internally

```

### Retrieving and Using Decrypted Keys

When loading credentials, the decryption occurs transparently:

```python
cred = await Credential.get("credential-id")
if cred.api_key:
    # Returns the decrypted plaintext

    clear_key = cred.api_key.get_secret_value()
    print(f"API Key: {clear_key}")

```

### Manual Encryption for Migrations

For bulk operations or data migrations, use the utility functions directly from [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py):

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

# Encrypt a value manually

cipher_text = encrypt_value("my-secret-api-key")

# Decrypt it back

plain_text = decrypt_value(cipher_text)
assert plain_text == "my-secret-api-key"

```

## Key Design Benefits

The encryption implementation in lfnovo/open-notebook provides several security advantages:

- **No plaintext in database**: Only Fernet tokens persist to the `credential` table, rendering database dumps useless to attackers without the encryption key.

- **Straightforward key rotation**: Changing `OPEN_NOTEBOOK_ENCRYPTION_KEY` and re-encrypting existing rows allows for rapid credential rotation.

- **Graceful legacy handling**: The `decrypt_value` function returns unencrypted values unchanged, ensuring backward compatibility during migrations.

- **API-level enforcement**: The `require_encryption_key` middleware in [`api/routers/credentials.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/credentials.py) ensures the encryption key is present before accepting credential-related requests.

## Summary

- Open Notebook uses **Fernet symmetric encryption** (AES-128-CBC with HMAC-SHA256) to protect API keys in the database.
- Encryption utilities reside in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), providing `encrypt_value()` and `decrypt_value()` functions.
- The `Credential` model in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) automatically encrypts fields on save and decrypts them on load.
- The encryption key is sourced from `OPEN_NOTEBOOK_ENCRYPTION_KEY` and derived via SHA-256 hashing.
- Legacy unencrypted data is handled gracefully without breaking existing functionality.

## Frequently Asked Questions

### What encryption algorithm does Open Notebook use for credential storage?

Open Notebook uses **Fernet symmetric encryption**, which implements AES-128 in CBC mode with PKCS7 padding and HMAC-SHA256 for authentication. This is provided by the Python `cryptography` library's `Fernet` class, ensuring authenticated encryption that prevents tampering and unauthorized decryption.

### How do I configure the encryption key for Open Notebook?

Set the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable to any secret string before starting the application. Alternatively, use `OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE` to specify a file path containing the key (useful for Docker secrets). The system automatically hashes the input with SHA-256 and base64-url-encodes it to create a valid Fernet key.

### Can I rotate the encryption key without losing existing credentials?

Yes, key rotation is supported. Change the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable and re-encrypt existing rows by loading each `Credential` object (which triggers decryption with the old key) and saving it again (which triggers encryption with the new key). The `decrypt_value` function includes fallback logic to return legacy unencrypted values unchanged, ensuring smooth transitions.

### What happens if the encryption key is lost?

If the `OPEN_NOTEBOOK_ENCRYPTION_KEY` is lost or changed without re-encrypting existing data, previously encrypted API keys become permanently unreadable. The Fernet tokens stored in the database cannot be decrypted without the exact key used during encryption. Always back up your encryption key in a secure secrets manager.