duck.utils.caching.encrypted

Encrypted cache wrapper using PyNaCl (libsodium).

Provides NaClEncryptor and EncryptedCache — a transparent encryption layer that wraps any CacheBase backend. The backend never sees plaintext. Encryption uses XSalsa20-Poly1305 AEAD: authenticated, single-pass, and ARM-safe.

Module Contents

Classes

EncryptedCache

Transparent encryption wrapper around any CacheBase backend.

NaClEncryptor

XSalsa20-Poly1305 AEAD encryptor backed by libsodium via PyNaCl.

Functions

generate_secret_key

Generate a 32-byte secret key for NaClEncryptor.

resolve_nacl_key

Ensures a raw secret key meets NaCl’s 32-byte requirement.

Data

DUCK_NACL_DERIVE_KEY_ENV

NACL_KEY_SIZE

NONCE_SIZE

API

duck.utils.caching.encrypted.DUCK_NACL_DERIVE_KEY_ENV

‘DUCK_NACL_DERIVE_KEY’

class duck.utils.caching.encrypted.EncryptedCache(backend: duck.utils.caching.CacheBase, secret_key: bytes)[source]

Bases: duck.utils.caching.CacheBase

Transparent encryption wrapper around any CacheBase backend.

Values are encrypted with NaClEncryptor before being passed to the inner backend and decrypted transparently on retrieval. The backend never sees or stores plaintext. Tampering or wrong-key errors cause the offending entry to be evicted immediately rather than returned.

All methods delegate locking to the inner backend — no double-locking. The async_* variants are forwarded to the inner backend’s async_* methods when available, falling back to the sync methods otherwise.

Parameters:
  • backend – Any CacheBase instance to use as the storage layer.

  • secret_key – 32-byte key from generate_secret_key().

Example:

    import os

    from duck.settings import SETTINGS
    from duck.caching import PersistentFileCache
    from duck.caching.encrypted import EncryptedCache, generate_secret_key

    # Unencrypted cache for general use.
    cache = PersistentFileCache("./var/cache")

    # Encrypted cache for sessions or any sensitive data.
    session_cache = EncryptedCache(
        backend=PersistentFileCache("./var/sessions"),
        secret_key=SETTINGS["SECRET_KEY"].encode(),
    )

    session_cache.set("sid:abc123", {"user_id": 42}, expiry=3600)
    data = session_cache.get("sid:abc123")

Initialization

async async_clear() None[source]

Async-safe version of clear.

async async_delete(key: str) None[source]

Async-safe version of delete.

Parameters:

key – Cache key to remove.

async async_get(key: str, default: Any = None) Any[source]

Async-safe version of get.

Parameters:
  • key – Cache key to look up.

  • default – Returned on miss, expiry, or tamper detection.

Returns:

The decrypted value or default.

async async_pop(key: str, default: Any = MISSING) Any[source]

Async-safe version of pop.

Parameters:
  • key – Cache key to retrieve and delete.

  • default – Returned when absent. Raises KeyError if omitted.

Returns:

The decrypted value or default.

Raises:

KeyError – When the key is missing and no default was given.

async async_set(key: str, value: Any, expiry: int | float | None = None) None[source]

Async-safe version of set.

Parameters:
  • key – Cache key.

  • value – JSON-serialisable value to store.

  • expiry – TTL in seconds.

clear() None[source]

Evict all entries from the underlying backend.

close() None[source]

Close the underlying backend.

delete(key: str) None[source]

Remove key from the backend. Silent if absent.

Parameters:

key – Cache key to remove.

get(key: str, default: Any = None) Any[source]

Retrieve and decrypt a value from the backend.

Returns default when the key is absent, expired, or the ciphertext fails authentication — tampered entries are evicted immediately and never returned.

Parameters:
  • key – Cache key to look up.

  • default – Returned on miss, expiry, or tamper detection.

Returns:

The decrypted value or default.

pop(key: str, default: Any = MISSING) Any[source]

Retrieve, decrypt, and atomically remove a value.

The raw value is popped from the backend first so that a tampered entry is always removed regardless of whether decryption succeeds.

Parameters:
  • key – Cache key to retrieve and delete.

  • default – Returned when absent. Raises KeyError if omitted.

Returns:

The decrypted value or default.

Raises:

KeyError – When the key is missing and no default was given.

set(key: str, value: Any, expiry: int | float | None = None) None[source]

Encrypt value and store it in the backend under key.

Parameters:
  • key – Cache key.

  • value – Any JSON-serialisable value to store.

  • expiry – TTL in seconds. None means no expiry.

Raises:

TypeError – When value is not JSON-serialisable.

duck.utils.caching.encrypted.NACL_KEY_SIZE

None

duck.utils.caching.encrypted.NONCE_SIZE: int

None

class duck.utils.caching.encrypted.NaClEncryptor(secret_key: bytes)[source]

XSalsa20-Poly1305 AEAD encryptor backed by libsodium via PyNaCl.

Authenticated encryption and integrity verification happen in a single pass — they are inseparable. A unique random nonce is generated per message and embedded in the output so decrypt() is fully self-contained with no external nonce management.

Builds cleanly on ARM, x86, and MIPS — anywhere libsodium compiles.

Parameters:

secret_key – 32 bytes from generate_secret_key().

Raises:

ValueError – When secret_key is not exactly 32 bytes.

Initialization

decrypt(data: bytes) Any[source]

Decrypt and deserialise bytes produced by encrypt().

Parameters:

data – Raw bytes from a previous encrypt() call.

Returns:

The original Python object.

Raises:

CryptoError – When data has been tampered with or was encrypted with a different key. Never silently returns bad data.

encrypt(value: Any) bytes[source]

Serialise value using Pickle then encrypt it.

A fresh random nonce is generated on every call so encrypting the same value twice always produces different ciphertext.

Parameters:

value – Any Pickle-serialisable Python object.

Returns:

Raw bytes containing the embedded nonce, ciphertext, and Poly1305 authentication tag. No base64 encoding applied.

Raises:

TypeError – When value is not Pickle-serialisable.

duck.utils.caching.encrypted.generate_secret_key() bytes[source]

Generate a 32-byte secret key for NaClEncryptor.

Run once and persist the result in an environment variable or a secrets manager. Never hardcode or commit the returned value.

Returns:

32 cryptographically random bytes.

duck.utils.caching.encrypted.resolve_nacl_key(secret_key: bytes) bytes[source]

Ensures a raw secret key meets NaCl’s 32-byte requirement.

Behaviour by key length:

  • Exactly 32 bytes — returned as-is, no processing.

  • Fewer than 32 bytes — derived to 32 bytes using BLAKE2b with a fixed 32-byte digest. This is stable: the same input always produces the same output, so previously encrypted data remains decryptable.

  • More than 32 bytes — raises ValueError unless the environment variable DUCK_NACL_DERIVE_KEY is set to "1", in which case BLAKE2b derivation is applied to compress the key to 32 bytes.

Parameters:

secret_key – The raw key bytes to resolve.

Returns:

A exactly 32-byte key safe to pass to nacl.secret.SecretBox.

Return type:

bytes

Raises:

ValueError – If the key is empty, or longer than 32 bytes without the opt-in environment variable set.