Secure Storage

fleche supports securing filesystem-based caches (PickleFile and CloudpickleFile) using HMAC-SHA256 signatures. This protects against untrusted code execution (RCE) via tampered pickle files.

Security is controlled via the FLECHE_SECRET_KEY environment variable, which accepts a hex-encoded secret key.

[ ]:
import os
from pathlib import Path
import tempfile
from fleche.storage.pickle_file import ValuePickleFile as PickleFile

# Set a strong hex-encoded secret key
os.environ["FLECHE_SECRET_KEY"] = "deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef0123"

# Initialize a storage backend. It will automatically load the key from the environment.
temp_dir = tempfile.TemporaryDirectory()
storage = PickleFile.with_pickle(root=temp_dir.name)

# Save a value
value_to_cache = {"status": "secure", "data": [1, 2, 3]}
digest_key = storage.save(value_to_cache)
print(f"Saved data with digest key: {digest_key[:8]}...")

When we load the data, fleche automatically verifies the signature. If it matches, the data is deserialized.

[2]:
loaded_value = storage.load(digest_key)
print("Loaded securely:", loaded_value)
Loaded securely: {'status': 'secure', 'data': [1, 2, 3]}

Tampering Protection

If an attacker tampers with the file, the signature verification will fail, preventing the potentially malicious payload from being deserialized.

[3]:
file_path = Path(temp_dir.name) / digest_key
original_content = file_path.read_bytes()

# Simulate an attacker appending malicious code or modifying the data
tampered_content = b"malicious_payload" + original_content
file_path.write_bytes(tampered_content)

try:
    storage.load(digest_key)
except KeyError as e:
    print("Security Error Caught:", e)
Invalid signature for cache entry. Potential tampering or key mismatch.
Security Error Caught: ('c38b58619f23f9507daf70dc7ef009512035480102031e8345c7e95fa254dbeb', 'Value present but failed signature check.')

Key Rotation

To support key rotation without invalidating existing caches, you can provide a colon-separated list of keys. fleche will sign new entries with the first key, and verify using any of the keys.

[ ]:
# We roll out a new key, but keep the old one for reading
os.environ["FLECHE_SECRET_KEY"] = "aabbccdd0011223344556677aabbccdd0011223344556677aabbccdd00112233:deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef0123"
storage_rotated = PickleFile.with_pickle(root=temp_dir.name)

# Restore the original (untampered) file to test reading with the old key
file_path.write_bytes(original_content)

# It successfully loads using the second key in the list!
rotated_loaded_value = storage_rotated.load(digest_key)
print("Loaded with rotated key:", rotated_loaded_value)