How Open Notebook Implements Credential Encryption: Field-Level Fernet Protection
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, 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:
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 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:
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:
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 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:
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:
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
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
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
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. - The encryption key is derived from
OPEN_NOTEBOOK_ENCRYPTION_KEYvia SHA-256 hashing and URL-safe base-64 encoding. encrypt_valueanddecrypt_valueprovide symmetric encryption with built-in support for legacy plaintext fallback.- The
Credentialmodel inopen_notebook/domain/credential.pyautomatically encrypts on save and decrypts on load, wrapping secrets in PydanticSecretStr. - Failed decryption attempts are handled gracefully, returning
UNDECRYPTABLEplaceholders 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. 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 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.
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 →