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

Python
def __init__(self, secret_key: Optional[str] = None) -> None
ParameterTypeRequiredDefaultNotes
secret_keyOptional[str]NoNoneIf None, the instance never attempts to set up Fernet — encrypt()/decrypt() always use the base64 fallback for that instance's entire lifetime, silently, with no warning logged (the warning is only logged if secret_key is provided but cryptography is missing — see below).

If secret_key is provided, _setup_fernet() is called:

Python · 9 lines
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 no secret_key, _setup_fernet is never called, self._fernet stays None, and every encrypt()/decrypt() call silently uses base64 — with no logged warning, unlike the case where secret_key is provided but cryptography is missing. Check .is_real_encryption explicitly 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

ExceptionWhen
cryptography.fernet.InvalidTokendecrypt() called with a token encrypted under a different key, or a corrupted/tampered token, when Fernet is active.
binascii.Errordecrypt() called on a non-base64 string when in fallback mode.
json.JSONDecodeErrordecrypt() succeeds at the crypto/encoding layer but the underlying plaintext isn't valid JSON (shouldn't happen in normal use since encrypt() always JSON-serializes first).

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

Python · 29 lines
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_encryption in production code paths — don't assume passing secret_key guarantees real encryption, since a missing cryptography install silently downgrades.
  • Store secret_key outside 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).
  • SensitiveDataMasker — complementary PII redaction (works on plaintext before encryption, not a replacement for it).
  • AIMemoryManager — constructs one MemoryEncryptor if encryption_key is passed.

Related pages