Engram supports client-side encryption — you encrypt content before sending it to the API. The server never sees your plaintext. Encrypted content is stored as-is on the Shelby blockchain.The API is a dumb pipe for encrypted data — it stores your encryptionProvider and encryptionMeta as passthrough metadata, never validating or enforcing specific encryption schemes.
Both providers derive the encryption key from your API key. The SDK and MCP server use PBKDF2 (100,000 iterations, SHA-256, random salt per memory) for key derivation. Manual implementations below use SHA-256(apiKey) for simplicity — both approaches produce AES-256-GCM ciphertext.
The first five options all use AES-256-GCM with the same key derivation and produce compatible ciphertext. The openssl CLI option uses AES-256-CBC + HMAC-SHA256 — see Shell Encryption below.
For locked-down environments where pip install is blocked. Uses Python’s built-in ctypes to call OpenSSL’s libcrypto directly — OpenSSL is pre-installed on virtually every Linux and macOS system.
This approach can cause segfaults in heavily sandboxed environments (WASM, restricted containers). If that happens, use the Shell approach below instead.
import ctypes, ctypes.util, hashlib, osdef _load_libcrypto(): path = ctypes.util.find_library('crypto') if path: return ctypes.CDLL(path) for name in ['libcrypto.so.3', 'libcrypto.so.1.1', 'libcrypto.dylib']: try: return ctypes.CDLL(name) except OSError: continue raise RuntimeError("OpenSSL libcrypto not found")_crypto = _load_libcrypto()_crypto.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p_crypto.EVP_aes_256_gcm.restype = ctypes.c_void_pdef encrypt_aes_gcm(plaintext_bytes, key_bytes): iv = os.urandom(12) ctx = _crypto.EVP_CIPHER_CTX_new() try: _crypto.EVP_EncryptInit_ex(ctx, _crypto.EVP_aes_256_gcm(), None, None, None) _crypto.EVP_CIPHER_CTX_ctrl(ctx, 0x9, 12, None) # SET_IVLEN _crypto.EVP_EncryptInit_ex(ctx, None, None, key_bytes, iv) out = ctypes.create_string_buffer(len(plaintext_bytes) + 16) out_len = ctypes.c_int(0) _crypto.EVP_EncryptUpdate(ctx, out, ctypes.byref(out_len), plaintext_bytes, len(plaintext_bytes)) ct = out.raw[:out_len.value] final_out, final_len = ctypes.create_string_buffer(16), ctypes.c_int(0) _crypto.EVP_EncryptFinal_ex(ctx, final_out, ctypes.byref(final_len)) tag = ctypes.create_string_buffer(16) _crypto.EVP_CIPHER_CTX_ctrl(ctx, 0x10, 16, tag) # GET_TAG return ct, iv, tag.raw finally: _crypto.EVP_CIPHER_CTX_free(ctx)# Usage:enc_key = hashlib.sha256(API_KEY.encode()).digest()ct, iv, tag = encrypt_aes_gcm(b"My secret data", enc_key)
For agents that cannot use any crypto library but can run shell commands via exec or subprocess. Uses AES-256-CBC + HMAC-SHA256 (Encrypt-then-MAC) — equally secure to AES-256-GCM. This is the same pattern used by TLS 1.2 and SSH.
Use encryptionProvider: "engram-aes-256-cbc-hmac" for shell-encrypted content to distinguish it from GCM-encrypted content. The API treats this as passthrough metadata.
Compression is skipped for encrypted content (compressed ciphertext would be larger)
visibility should be "private" for encrypted content
Content with detected credentials (API keys, passwords) in "share" mode is rejected with 422
The encryptionMeta is stored in the database (not encrypted) so you can retrieve the IV and auth tag for decryption
Encrypted content is not indexed for content search. However, encrypted memories are still discoverable via key search, tag search, type filtering, and vector search (embeddings are stored unencrypted). Use descriptive keys and tags at write time.