# How Open Notebook Implements Credential Encryption: Field-Level Fernet Protection

> Learn how Open Notebook implements field-level Fernet encryption for credentials. Discover how API keys are secured before saving to SurrealDB.

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

---

**Open Notebook implements credential encryption using field-level Fernet (AES-128-CBC + HMAC-SHA256) encryption, deriving keys from the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable and automatically encrypting API keys before persisting them to SurrealDB while gracefully handling legacy plaintext values.**

The `lfnovo/open-notebook` repository protects provider API keys with a transparent encryption layer built on Python's `cryptography` library. Its **credential encryption** strategy ensures that sensitive API tokens are encrypted at rest in SurrealDB, decrypted automatically when models are loaded, and remains backward-compatible with legacy plaintext records.

## Encryption Key Derivation and Loading

All cryptographic operations rely on a single Fernet key derived from an administrator-supplied secret. The key loading logic resides in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py), where `_get_or_create_encryption_key` reads the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable or its Docker `*_FILE` secret variant via `get_secret_from_env`.

If the variable is missing, the function raises a `ValueError`. Otherwise, `_ensure_fernet_key` hashes the raw string with SHA-256 and encodes the digest into a URL-safe base-64 string:

```python
def _ensure_fernet_key(key: str) -> str:
    derived = hashlib.sha256(key.encode()).digest()
    return base64.urlsafe_b64encode(derived).decode()

```

The resulting Fernet key is cached lazily in `_ENCRYPTION_KEY`, and a `Fernet` instance is created on demand by `get_fernet()`.

## Encrypt and Decrypt Utilities

The same [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) module exposes `encrypt_value` and `decrypt_value`, which wrap the Fernet instance to protect arbitrary strings. **Field-level encryption** is applied only to the specific values that require secrecy, leaving the rest of the record untouched.

### Encrypting Values

The `encrypt_value` function encodes the plaintext string and returns a URL-safe base-64 token:

```python
def encrypt_value(value: str) -> str:
    fernet = get_fernet()
    return fernet.encrypt(value.encode()).decode()

```

### Decrypting Values and Legacy Support

`decrypt_value` reverses the operation but includes special handling for legacy data and key rotation scenarios:

```python
def decrypt_value(value: str) -> str:
    fernet = get_fernet()
    try:
        return fernet.decrypt(value.encode()).decode()
    except InvalidToken:
        if looks_like_fernet_token(value):
            raise ValueError("Decryption failed: data appears to be encrypted ...")
        return value

```

The helper `looks_like_fernet_token` inspects the stored value for minimum size and block alignment. When the token looks encrypted but cannot be decrypted, the function raises a clear error indicating the encryption key has changed. If the value is plain text, it returns the original string unchanged, enabling seamless migration for legacy records.

## Credential Model Integration

The `Credential` model in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) wires the encryption utilities directly into the SurrealDB persistence layer. This design guarantees that no API key is written to disk in plaintext, yet application code interacts with the model as if the field were a normal string.

### Automatic Encryption Before Save

During serialization, `_prepare_save_data` intercepts the `api_key` field, extracts the underlying secret from its **SecretStr** wrapper, and replaces it with ciphertext:

```python
if key == "api_key":
    if self.api_key:
        secret_value = self.api_key.get_secret_value()
        data["api_key"] = encrypt_value(secret_value)
    else:
        data["api_key"] = None

```

### Transparent Decryption on Load

When records are fetched via `get`, `get_all`, or `_from_db_row`, the raw database value is passed through `decrypt_value` and re-wrapped as a `SecretStr`:

```python
api_key_val = row.get("api_key")
if api_key_val and isinstance(api_key_val, str):
    decrypted = decrypt_value(api_key_val)
    row["api_key"] = SecretStr(decrypted)

```

### Graceful Error Handling

If decryption fails because the Fernet key has rotated, the model catches the error per-row. Instead of crashing the entire query, the credential is returned with an **UNDECRYPTABLE** placeholder and a descriptive `decryption_error` field explaining the mismatch. This allows administrators to identify and re-provision affected credentials without data loss.

## Stand-Alone and Model Usage Examples

The encryption layer can be invoked directly or consumed transparently through the domain model.

### Encrypt an API Key Directly

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

plain = "sk-my-super-secret-key"
encrypted = encrypt_value(plain)
print("Encrypted:", encrypted)

assert decrypt_value(encrypted) == plain

```

### Persist and Retrieve a Credential

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

async def demo():
    cred = Credential(
        name="MyOpenAI",
        provider="openai",
        modalities=["language"],
        api_key=SecretStr("sk-my-openai-key")
    )
    await cred.save()

    fetched = await Credential.get(cred.id)
    print(fetched.api_key.get_secret_value())

asyncio.run(demo())

```

### Detect a Key Rotation Error

```python
try:
    cred = await Credential.get(some_id)
except ValueError as exc:
    print("Bad encryption key:", exc)

```

## Summary

- **Open Notebook** uses Fernet (AES-128-CBC + HMAC-SHA256) for field-level credential encryption, implemented in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py).
- The encryption key is derived from `OPEN_NOTEBOOK_ENCRYPTION_KEY` via SHA-256 hashing and URL-safe base-64 encoding.
- `encrypt_value` and `decrypt_value` provide symmetric encryption with built-in support for legacy plaintext fallback.
- The `Credential` model in [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) automatically encrypts on save and decrypts on load, wrapping secrets in Pydantic `SecretStr`.
- Failed decryption attempts are handled gracefully, returning `UNDECRYPTABLE` placeholders with explanatory errors rather than crashing the application.

## Frequently Asked Questions

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

Open Notebook uses **Fernet** from Python's `cryptography` library, which combines AES-128 in CBC mode with HMAC-SHA256 for authenticated encryption. This ensures both confidentiality and integrity for stored API keys.

### Where is the encryption key configured?

The application reads the `OPEN_NOTEBOOK_ENCRYPTION_KEY` environment variable, or its Docker secret file variant, through the `get_secret_from_env` helper as defined in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py). The key is supplied at runtime and is never stored inside the repository or database.

### How does Open Notebook handle old credentials that were not encrypted?

The `decrypt_value` function in [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) detects non-Fernet values via `looks_like_fernet_token` and returns them unchanged. This allows legacy plaintext records to continue functioning without requiring a bulk migration.

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

If the stored token resembles a Fernet ciphertext but fails decryption, the system raises a `ValueError` indicating the key is incorrect. At the model level, this error is caught and surfaced as an `UNDECRYPTABLE` status with a `decryption_error` message, preventing application crashes while flagging the affected credential.