How Open Notebook Implements Fernet Encryption for Secure Credential Storage
Open Notebook uses the cryptography library's Fernet scheme to encrypt API keys and credentials at the field level, deriving a 32-byte key from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable and providing transparent encryption/decryption through utility functions in open_notebook/utils/encryption.py.
The lfnovo/open-notebook repository protects sensitive provider credentials using Fernet encryption, a symmetric encryption standard that combines AES-128-CBC with HMAC-SHA256 for authenticated encryption. This implementation ensures that API keys and secrets never persist in plaintext within the database, while maintaining seamless access for authorized application components.
Fernet Encryption Architecture
The encryption layer resides in open_notebook/utils/encryption.py and serves as the central utility for all credential protection. Rather than storing raw encryption keys directly, the system accepts arbitrary strings through environment variables and normalizes them into valid Fernet keys using cryptographic hashing.
The ProviderConfig model in open_notebook/domain/provider_config.py consumes these utilities to automatically encrypt values before persistence and decrypt them on access, ensuring developers never handle plaintext secrets in business logic.
Key Derivation and Environment Configuration
Environment Variable Setup
The system sources its encryption material from the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable. For Docker deployments, it supports a file-based fallback via OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE, reading the key from a secrets file rather than the environment.
Any string provided through these channels is accepted, removing the burden of generating Fernet-specific base64-encoded keys manually.
SHA-256 Key Normalization
Since Fernet requires a 32-byte URL-safe base64-encoded key, the implementation derives valid keys from arbitrary input strings using the _ensure_fernet_key function. This process hashes the input with SHA-256 and base64-url-encodes the digest, producing a deterministic 32-byte key that satisfies Fernet requirements.
The _get_or_create_encryption_key function orchestrates this retrieval and normalization process, ensuring consistent key material across application restarts.
Encryption and Decryption Workflow
Lazy Fernet Instance Creation
The get_fernet() function implements lazy initialization, constructing the Fernet object only upon first use. This design prevents application crashes during import if the encryption key is temporarily unavailable, deferring key validation until the first encryption or decryption operation occurs.
Field-Level Encryption Process
When persisting credentials, the encrypt_value(plain_text) function performs the following operations:
- Encodes the plaintext string to UTF-8 bytes
- Encrypts using the Fernet instance (AES-128-CBC encryption with HMAC-SHA256 authentication)
- Returns a URL-safe base64 string suitable for database storage
During retrieval, decrypt_value(possibly_encrypted) reverses this process, converting the token back to the original plaintext string.
Graceful Legacy Data Handling
The implementation includes backward compatibility for unencrypted legacy data. The looks_like_fernet_token helper checks whether a stored value matches Fernet's token format. If the value does not resemble a Fernet token, decrypt_value returns it unchanged, allowing seamless migration from plaintext storage.
If decryption fails due to an incorrect key, the system raises a clear ValueError rather than returning corrupted data, preventing silent authentication failures with invalid API keys.
Integration with the Credential Model
The ProviderConfig class in open_notebook/domain/provider_config.py integrates encryption directly into its property accessors. The setter automatically encrypts values using encrypt_value, while the getter decrypts them using decrypt_value before returning the plaintext to consuming code.
This pattern ensures that the database always contains encrypted blobs (prefixed with gAAAAAB in base64), while the application works with decrypted strings transparently. The open_notebook/ai/key_provider.py module retrieves these decrypted keys to instantiate AI provider clients without exposing secrets in logs or stack traces.
Security Properties and Guarantees
Authenticated Encryption: Fernet provides both confidentiality and integrity protection. The ciphertext includes an HMAC-SHA256 signature that prevents tampering, ensuring that modified or corrupted credentials are detected during decryption.
Deterministic Key Derivation: The SHA-256 derivation ensures that the same environment variable always produces the same encryption key, while allowing users to provide simple passphrases rather than cryptographically random 32-byte strings.
Fail-Safe Defaults: The lazy initialization and legacy data handling ensure that encryption enhancements do not break existing deployments or prevent application startup in development environments.
Practical Implementation Examples
Basic Encryption and Decryption
from open_notebook.utils.encryption import encrypt_value, decrypt_value
# Encrypt a credential before database storage
api_key = "sk-abc123-secret"
encrypted_key = encrypt_value(api_key)
# Result: 'gAAAAABlY...' (URL-safe base64 Fernet token)
# Decrypt when retrieving from storage
plain_key = decrypt_value(encrypted_key)
# Returns: 'sk-abc123-secret'
Model Integration Pattern
# Simplified pattern from open_notebook/domain/provider_config.py
class ProviderConfig(BaseModel):
_encrypted_api_key: str = ""
@property
def api_key(self) -> str:
"""Return the decrypted API key for use in API clients."""
return decrypt_value(self._encrypted_api_key)
@api_key.setter
def api_key(self, value: str) -> None:
"""Store the API key as an encrypted Fernet token."""
self._encrypted_api_key = encrypt_value(value)
Environment Configuration
# Set the encryption key in your environment
export OPEN_NOTEBOOK_ENCRYPTION_KEY="your-secret-passphrase-here"
# Or use Docker secrets
export OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE="/run/secrets/encryption_key"
Summary
- Fernet encryption in Open Notebook uses the
cryptographylibrary to provide authenticated AES-128-CBC encryption with HMAC-SHA256 integrity verification. - The implementation resides in
open_notebook/utils/encryption.py, providingencrypt_value()anddecrypt_value()functions for field-level protection. - Keys are derived from the
OPEN_NOTEBOOK_ENCRYPTION_KEYenvironment variable using SHA-256 hashing, accepting arbitrary strings rather than requiring base64-encoded bytes. - The
ProviderConfigmodel inopen_notebook/domain/provider_config.pytransparently encrypts credentials before database storage and decrypts them on access. - Legacy plaintext credentials remain readable through the
looks_like_fernet_tokencheck, ensuring backward compatibility during migrations.
Frequently Asked Questions
What encryption algorithm does Open Notebook use for credential storage?
Open Notebook uses Fernet symmetric encryption from the Python cryptography library. This scheme combines AES-128 in CBC mode for confidentiality with HMAC-SHA256 for message authentication, ensuring both privacy and integrity of stored API keys.
How do I configure the encryption key for production deployments?
Set the OPEN_NOTEBOOK_ENCRYPTION_KEY environment variable to a secure, random string before starting the application. For Docker Swarm or Kubernetes deployments, use OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE to specify a path to a secrets file. The system automatically hashes your input with SHA-256 to derive the 32-byte Fernet key.
What happens if I lose or change the encryption key?
If you change the encryption key, existing encrypted credentials cannot be decrypted and decrypt_value() will raise a ValueError. However, legacy plaintext credentials (stored before encryption was implemented) continue to work normally. Always back up your OPEN_NOTEBOOK_ENCRYPTION_KEY value, as losing it renders encrypted API keys permanently irretrievable.
Does the encryption impact application performance?
The impact is negligible for typical API key storage and retrieval. Fernet encryption operates on small strings (API keys and tokens) and uses optimized AES implementations from the cryptography library. The lazy initialization in get_fernet() ensures the encryption context is reused across operations within the same process.
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 →