Credential Encryption Using Fernet in Open‑Notebook: How API Keys Are Secured

Open‑Notebook uses Fernet symmetric encryption from the cryptography library to encrypt API keys before storing them in the database, deriving the encryption key from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable.

The lfnovo/open-notebook repository implements a robust security layer for sensitive provider credentials. Credential encryption using Fernet ensures that API keys never persist in plaintext, protecting secrets at rest while maintaining transparent access patterns for the application layer.

Where the Encryption Key Comes From

Open‑Notebook reads the master secret from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable. In containerized deployments, you can alternatively mount a Docker secret to a file path specified by OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE.

The resolution logic lives in open_notebook/utils/encryption.py at line 62 inside the _get_or_create_encryption_key function. If neither variable is present, the module raises a clear ValueError, forcing operators to configure a secret before any encrypted data can be stored.

How Fernet Keys Are Derived

Fernet expects a 32‑byte URL‑safe base‑64 encoded key, but operators can supply any arbitrary passphrase. The _ensure_fernet_key function at line 104 in open_notebook/utils/encryption.py bridges this gap by:

  1. Hashing the input string using SHA‑256.
  2. Base‑64 encoding the digest to produce a deterministic Fernet key.

This approach guarantees that the same passphrase always generates the same encryption key, simplifying backups and disaster recovery without storing the raw Fernet key itself.

The Encryption and Decryption API

The encryption module exposes three core utilities that handle all cryptographic operations:

  • encrypt_value(value: str) → str – Implemented at line 28 in open_notebook/utils/encryption.py, this function encrypts a plaintext string and returns a URL‑safe base‑64 token.
  • decrypt_value(value: str) → str – Starting at line 66, this function attempts Fernet decryption. If the token is valid, it returns the plaintext; if the token appears to be a Fernet token but decryption fails, it raises a ValueError.
  • looks_like_fernet_token(s: str) → bool – Located at line 45, this helper inspects token length and structure to distinguish encrypted blobs from legacy plaintext.
from open_notebook.utils.encryption import encrypt_value, decrypt_value

# Encrypting a secret

encrypted = encrypt_value("sk-secret-key")

# Returns a Fernet token like: 'gAAAAAB...'

# Decrypting it back

decrypted = decrypt_value(encrypted)

# Returns: 'sk-secret-key'

Integration with the Credential Model

The Credential domain model in open_notebook/domain/credential.py treats API keys as Optional[SecretStr] (line 61). This Pydantic type ensures that secrets are never accidentally logged or serialized into JSON responses.

When a credential is saved, the repository layer calls encrypt_value via the ObjectModel._to_db_row helper before persisting the string to the database. When retrieving credentials, the Credential.get method (line 55) and Credential.get_all method (line 71) invoke decrypt_value on the stored string, then wrap the result back in a SecretStr.

This flow is transparent to callers, but any decryption failure is captured in the decryption_error field for UI display rather than crashing the request.

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

async def create_and_retrieve():
    # Create with plaintext; it gets encrypted before storage

    cred = Credential(
        name="Production",
        provider="openai",
        modalities=["language"],
        api_key=SecretStr("sk-my-secret-key"),
    )
    await cred.save()  # encrypt_value runs under the hood

    
    # Retrieve and decrypt automatically

    fetched = await Credential.get(cred.id)
    api_key = fetched.api_key.get_secret_value()
    print(f"Decrypted API key: {api_key[:4]}...")

Handling Legacy Data and Key Rotation

Open‑Notebook includes safety checks for operational continuity.

Backward compatibility is handled by decrypt_value at line 66: if looks_like_fernet_token returns False, the function assumes the stored value is legacy plaintext and returns it unchanged. This allows the system to read credentials that were never encrypted without requiring a migration script.

Key rotation errors are captured at line 87 in open_notebook/utils/encryption.py. When InvalidToken is raised (indicating the encryption key changed), the Credential.get_all method (lines 86‑95) records a decryption_error message instead of propagating the exception. Administrators can audit these errors without breaking the UI.

import asyncio
from open_notebook.domain.credential import Credential

async def audit_credentials():
    creds = await Credential.get_all()
    for c in creds:
        if c.decryption_error:
            print(f"{c.name}: DECRYPTION FAILED – {c.decryption_error}")
        else:
            print(f"{c.name}: OK")

Summary

  • Key derivation: _ensure_fernet_key in open_notebook/utils/encryption.py creates deterministic Fernet keys from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable using SHA‑256 hashing.
  • Encryption: encrypt_value encrypts API keys before database storage, while decrypt_value handles retrieval and automatically manages legacy plaintext.
  • Model integration: The Credential class in open_notebook/domain/credential.py uses Pydantic SecretStr to prevent accidental exposure and invokes encryption utilities during save/load operations.
  • Error handling: Decryption failures are surfaced in the decryption_error field, allowing graceful degradation when keys rotate or tokens corrupt.

Frequently Asked Questions

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

Set the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable to a strong passphrase before starting the application. For Docker deployments, you can use OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE to point to a file containing the secret. If neither is configured, the application raises a ValueError on startup to prevent unencrypted storage.

What happens if I change the encryption key after credentials are stored?

If you rotate the OPEN_NOTEBOOK_ENCRYPTION_KEY, existing encrypted credentials will fail to decrypt. The decrypt_value function raises InvalidToken when this occurs, and the Credential.get_all method captures this in the decryption_error field. You must re‑encrypt credentials with the new key or restore the original key to recover access.

Can Open‑Notebook read unencrypted credentials from older versions?

Yes. The decrypt_value function in open_notebook/utils/encryption.py includes a looks_like_fernet_token check. If a stored credential does not match the Fernet token format, the function returns the raw value unchanged, allowing seamless migration from legacy plaintext storage to encrypted storage.

Why does the code use SecretStr instead of plain strings?

The Credential model uses Pydantic's SecretStr type (defined in open_notebook/domain/credential.py) to prevent accidental logging or JSON serialization of API keys. The actual secret is only exposed when you explicitly call .get_secret_value(), adding a safety layer against debug output and log leaks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →