Open Notebook Credential Encryption: How API Keys Are Protected in the Database
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, 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 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 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:
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:
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:
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
credentialtable, rendering database dumps useless to attackers without the encryption key. -
Straightforward key rotation: Changing
OPEN_NOTEBOOK_ENCRYPTION_KEYand re-encrypting existing rows allows for rapid credential rotation. -
Graceful legacy handling: The
decrypt_valuefunction returns unencrypted values unchanged, ensuring backward compatibility during migrations. -
API-level enforcement: The
require_encryption_keymiddleware inapi/routers/credentials.pyensures 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, providingencrypt_value()anddecrypt_value()functions. - The
Credentialmodel inopen_notebook/domain/credential.pyautomatically encrypts fields on save and decrypts them on load. - The encryption key is sourced from
OPEN_NOTEBOOK_ENCRYPTION_KEYand 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →