MemoryEncryptor
On this page
Purpose
Optional symmetric encryption for memory content before it's persisted or transmitted. Uses Fernet (AES-128-CBC + HMAC-SHA256) from the cryptography package when available; degrades to plain base64 encoding — not encryption — if cryptography isn't installed.
Constructor
def __init__(self, secret_key: Optional[str] = None) -> None
If secret_key is provided, _setup_fernet() is called:
def _setup_fernet(self, secret_key: str) -> None:
try:
from cryptography.fernet import Fernet
raw = hashlib.sha256(secret_key.encode()).digest()
fernet_key = base64.urlsafe_b64encode(raw)
self._fernet = Fernet(fernet_key)
self._key_hash = hashlib.sha256(secret_key.encode()).hexdigest()[:16]
except ImportError:
logger.warning("cryptography package not installed — falling back to base64 (NOT SECURE).")
The key is derived via SHA-256 of your secret_key string, then base64url-encoded to form a valid Fernet key — so any string works as secret_key regardless of length (unlike raw Fernet keys, which must be 32 url-safe base64-encoded bytes).
[!DANGER] No warning if secret_key is never passed at all If you simply construct
MemoryEncryptor()with nosecret_key,_setup_fernetis never called,self._fernetstaysNone, and everyencrypt()/decrypt()call silently uses base64 — with no logged warning, unlike the case wheresecret_keyis provided butcryptographyis missing. Check.is_real_encryptionexplicitly if you need to confirm real encryption is active.
Public methods
encrypt(data: Any) -> str
Serializes data via json.dumps(data, ensure_ascii=False), then either Fernet-encrypts (if configured) or base64-encodes the JSON string. Returns a str in both cases.
decrypt(token: str) -> Any
Reverses encrypt() — Fernet-decrypts or base64-decodes, then json.loads(). Using decrypt() with a MemoryEncryptor instance configured differently than the one that produced token (e.g. a different secret_key, or Fernet vs. base64 mode) will raise (cryptography.fernet.InvalidToken for a wrong Fernet key, or binascii.Error/UnicodeDecodeError/json.JSONDecodeError for base64/JSON mismatches).
is_real_encryption (property)
True only if secret_key was provided and cryptography was importable at construction time. This is the only reliable way to distinguish "actually encrypted" from "base64 only" — don't infer it from whether secret_key was passed, since a missing cryptography package silently downgrades even a provided key.
key_fingerprint (property)
Returns the first 16 hex characters of sha256(secret_key), or None if no key was configured (or Fernet setup failed). Useful for confirming two MemoryEncryptor instances were constructed with the same key, without ever exposing the key itself.
Raises
Thread safety
Both encrypt/decrypt are read-only with respect to instance state (self._fernet is set once at construction and never reassigned) — safe to call concurrently from multiple threads on the same instance. The underlying cryptography.fernet.Fernet object is documented by that library as safe for concurrent use.
Complexity
O(size of data) for JSON serialization plus O(size of serialized string) for the encryption/encoding pass — both linear.
Examples
from zerda.memory import MemoryEncryptor
# 1. Basic — real encryption (requires `pip install cryptography`)
enc = MemoryEncryptor(secret_key="a-strong-passphrase")
token = enc.encrypt({"input": "secret plan", "output": "acknowledged"})
print(enc.is_real_encryption) # True, if cryptography is installed
data = enc.decrypt(token)
print(data) # {"input": "secret plan", "output": "acknowledged"}
# 2. Intermediate — no key at all (base64 fallback, no warning logged)
enc_plain = MemoryEncryptor()
token2 = enc_plain.encrypt({"x": 1})
print(enc_plain.is_real_encryption) # False
# 3. Advanced — verify two instances share the same key without exposing it
enc_a = MemoryEncryptor(secret_key="shared-secret")
enc_b = MemoryEncryptor(secret_key="shared-secret")
assert enc_a.key_fingerprint == enc_b.key_fingerprint
# 4. Error handling — decrypting with the wrong key
enc_wrong = MemoryEncryptor(secret_key="different-secret")
try:
enc_wrong.decrypt(token)
except Exception as e:
print(type(e).__name__) # InvalidToken (if cryptography active) or a decode error
# 5. Production — always check is_real_encryption before assuming ciphertext is opaque
if not enc.is_real_encryption:
raise RuntimeError("cryptography package required for production encryption")
Best practices
- Always check
.is_real_encryptionin production code paths — don't assume passingsecret_keyguarantees real encryption, since a missingcryptographyinstall silently downgrades. - Store
secret_keyoutside your source code (environment variable, secrets manager) — this class does not manage key storage for you.
Common mistakes
- Constructing
MemoryEncryptor()with no key, believing content is "protected" — it's plain base64, trivially reversible. - Assuming
decrypt()failures always mean "wrong key" — could also mean the token was produced in fallback mode and you're now decrypting with a Fernet-active instance (or vice versa).
Related APIs
SensitiveDataMasker— complementary PII redaction (works on plaintext before encryption, not a replacement for it).AIMemoryManager— constructs oneMemoryEncryptorifencryption_keyis passed.