# How Fernet Encryption Secures Credentials in SurrealDB: Open Notebook's Security Architecture

> Learn how Open Notebook uses Fernet encryption to secure API keys in SurrealDB. Discover how this symmetric encryption protects your secrets from database breaches without the original key.

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

---

**Open Notebook encrypts API keys using Fernet symmetric encryption before storing them in SurrealDB, ensuring that even database compromises cannot expose plaintext secrets without the original encryption key.**

Open Notebook stores sensitive third-party API credentials—such as OpenAI and Anthropic keys—inside SurrealDB records. Because these secrets must remain confidential even if the database is breached, the project implements application-layer encryption using the Fernet implementation from Python's **cryptography** library. This design ensures that **Fernet encryption secures credentials in SurrealDB** by transforming plaintext API keys into encrypted tokens before they ever reach the database server.

## Key Derivation from Environment Variables

The encryption system derives its Fernet key from the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable (or a Docker secrets file variant). In [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), the function `_ensure_fernet_key` handles this transformation:

1. The raw string from the environment is hashed using SHA-256
2. The 32-byte digest is encoded as URL-safe base64
3. This produces a Fernet-compatible key deterministically

This approach means the same passphrase always generates the same encryption key, while the actual cryptographic operations (AES-128-CBC encryption with HMAC-SHA256 authentication) are handled by the Fernet specification.

```python

# From open_notebook/utils/encryption.py (lines 5-12)

def _ensure_fernet_key(key: str) -> bytes:
    """Ensure the key is 32 bytes and URL-safe base64 encoded."""
    if isinstance(key, str):
        key = key.encode()
    # Hash with SHA-256 and encode as Fernet-compatible base64

    return base64.urlsafe_b64encode(hashlib.sha256(key).digest())

```

The `get_fernet()` function (lines 15-25) instantiates the `Fernet` object using this derived key, which is then used throughout the application for encrypt and decrypt operations.

## Encryption Workflow Before Storage

When a provider configuration is saved, each `SecretStr` containing an API key undergoes encryption through the `encrypt_value` function. In [`open_notebook/domain/provider_config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/provider_config.py) (lines 123-128), the application extracts the secret value and encrypts it before persistence:

```python

# Simplified flow from open_notebook/domain/provider_config.py

from open_notebook.utils.encryption import encrypt_value
from pydantic import SecretStr

def save_credentials(api_key: SecretStr):
    # Convert SecretStr to plain string, then encrypt

    encrypted = encrypt_value(api_key.get_secret_value())
    # Store encrypted string in SurrealDB

    db_record["api_key"] = encrypted

```

The `encrypt_value` function calls `Fernet.encrypt`, returning a UTF-8 string safe for SurrealDB storage. This ensures that the literal API key never exists in plaintext within the database.

## Decryption and Error Handling on Retrieval

When credentials are read from SurrealDB, the `decrypt_value` function reverses the process. Implemented in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) (lines 67-78), this function includes robust error handling for configuration mismatches:

- If the stored value is a valid Fernet token but decryption fails, the system raises an explicit error indicating a mismatched encryption key
- If the value is not a Fernet token (legacy unencrypted data), it returns the raw string unchanged to support migration scenarios

```python

# From open_notebook/utils/encryption.py (lines 67-78)

def decrypt_value(encrypted_value: str) -> str:
    try:
        return get_fernet().decrypt(encrypted_value.encode()).decode()
    except Exception:
        # If it looks like a Fernet token but failed, the key is wrong

        if encrypted_value.startswith("gAAAAA"):
            raise ValueError("Invalid encryption key")
        # Otherwise return raw value (legacy unencrypted support)

        return encrypted_value

```

## Fernet Token Structure and Security Guarantees

The encrypted tokens stored in SurrealDB follow the Fernet specification, providing both confidentiality and integrity protection. Each token is a base64-encoded string containing:

- **Version** (1 byte): Protocol version identifier
- **Timestamp** (8 bytes): Creation time for TTL validation
- **IV** (16 bytes): Random initialization vector for AES-CBC
- **Ciphertext** (variable, multiple of 16 bytes): The encrypted API key
- **HMAC** (32 bytes): SHA-256 authentication tag

This structure guarantees that tampering with any component of the token is detected during decryption, while AES-128-CBC ensures the underlying secret remains confidential.

## Complete Implementation Example

The following example demonstrates the full encryption lifecycle as implemented in the Open Notebook repository:

```python

# Configuration: Set via environment or .env file

# OPEN_NOTEBOOK_ENCRYPTION_KEY=my-super-secret-passphrase

from open_notebook.utils.encryption import encrypt_value, decrypt_value

# Encrypt before storing to SurrealDB

raw_api_key = "sk-abc123def456xyz"
encrypted_token = encrypt_value(raw_api_key)
print(f"Stored in DB: {encrypted_token}")

# Output: gAAAAABlY...

# Decrypt when retrieving for API calls

decrypted_key = decrypt_value(encrypted_token)
assert decrypted_key == raw_api_key

```

The [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) module also utilizes these same encryption helpers when serializing credential models, ensuring consistent protection across all secret storage operations.

## Summary

- **Fernet encryption secures credentials in SurrealDB** by applying AES-128-CBC with HMAC-SHA256 authentication before any data reaches the database.
- The encryption key derives from `OPEN_NOTEBOOK_ENCRYPTION_KEY` via SHA-256 hashing and base64 encoding in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py).
- API keys are encrypted via `encrypt_value` when saving provider configurations in [`open_notebook/domain/provider_config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/provider_config.py).
- Decryption handles both encrypted tokens and legacy plaintext values, with explicit errors for key mismatches.
- The token format provides cryptographic guarantees that prevent both unauthorized disclosure and undetected tampering.

## Frequently Asked Questions

### How does Open Notebook handle encryption key rotation?

Open Notebook determines the encryption key solely from the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable at runtime. There is no built-in automatic key rotation mechanism; changing the key requires updating the environment variable and re-encrypting existing credentials. If you attempt to decrypt data with a different key than was used for encryption, the system raises a `ValueError` indicating an invalid encryption key.

### What happens if the OPEN_NOTEBOOK_ENCRYPTION_KEY is lost?

If the `OPEN_NOTEBOOK_ENCRYPTION_KEY` is lost or changed, all encrypted API credentials stored in SurrealDB become permanently inaccessible. The Fernet encryption used is symmetric, meaning the same key is required for both encryption and decryption. Without the original key, the encrypted tokens cannot be recovered, and you would need to regenerate new API keys from the third-party providers.

### Does Fernet encryption protect against database administrators?

Yes, Fernet encryption protects against database administrators or anyone with direct SurrealDB access. Since the encryption occurs at the application layer before data reaches the database, a database dump or SQL query only reveals encrypted tokens (strings starting with `gAAAAA`). The actual plaintext API keys exist only in memory during the brief moments when `decrypt_value` is called and the result is passed to the API client.

### Why did Open Notebook choose Fernet over other encryption methods?

Open Notebook chose Fernet from the **cryptography** library because it provides authenticated encryption (AES-128-CBC with HMAC-SHA256) in a standard, well-tested format. Unlike raw AES implementations, Fernet tokens include built-in tamper detection and version information, preventing ciphertext manipulation attacks. The library also handles complex cryptographic details—such as secure random IV generation and HMAC computation—correctly, reducing the risk of implementation errors common in custom crypto schemes.