{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": "# Secure Storage\n\n`fleche` supports securing filesystem-based caches (`PickleFile` and `CloudpickleFile`) using HMAC-SHA256 signatures. This protects against untrusted code execution (RCE) via tampered pickle files.\n\nSecurity is controlled via the `FLECHE_SECRET_KEY` environment variable, which accepts a hex-encoded secret key." }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-03-22T19:56:48.060819Z", "iopub.status.busy": "2026-03-22T19:56:48.060615Z", "iopub.status.idle": "2026-03-22T19:56:48.590986Z", "shell.execute_reply": "2026-03-22T19:56:48.590151Z" } }, "outputs": [], "source": "import os\nfrom pathlib import Path\nimport tempfile\nfrom fleche.storage.pickle_file import ValuePickleFile as PickleFile\n\n# Set a strong hex-encoded secret key\nos.environ[\"FLECHE_SECRET_KEY\"] = \"deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef0123\"\n\n# Initialize a storage backend. It will automatically load the key from the environment.\ntemp_dir = tempfile.TemporaryDirectory()\nstorage = PickleFile.with_pickle(root=temp_dir.name)\n\n# Save a value\nvalue_to_cache = {\"status\": \"secure\", \"data\": [1, 2, 3]}\ndigest_key = storage.save(value_to_cache)\nprint(f\"Saved data with digest key: {digest_key[:8]}...\")" }, { "cell_type": "markdown", "metadata": {}, "source": [ "When we load the data, `fleche` automatically verifies the signature. If it matches, the data is deserialized." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2026-03-22T19:56:48.593145Z", "iopub.status.busy": "2026-03-22T19:56:48.592858Z", "iopub.status.idle": "2026-03-22T19:56:48.596892Z", "shell.execute_reply": "2026-03-22T19:56:48.596059Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loaded securely: {'status': 'secure', 'data': [1, 2, 3]}\n" ] } ], "source": [ "loaded_value = storage.load(digest_key)\n", "print(\"Loaded securely:\", loaded_value)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Tampering Protection\n", "\n", "If an attacker tampers with the file, the signature verification will fail, preventing the potentially malicious payload from being deserialized." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2026-03-22T19:56:48.598687Z", "iopub.status.busy": "2026-03-22T19:56:48.598499Z", "iopub.status.idle": "2026-03-22T19:56:48.603751Z", "shell.execute_reply": "2026-03-22T19:56:48.603021Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Invalid signature for cache entry. Potential tampering or key mismatch.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Security Error Caught: ('c38b58619f23f9507daf70dc7ef009512035480102031e8345c7e95fa254dbeb', 'Value present but failed signature check.')\n" ] } ], "source": [ "file_path = Path(temp_dir.name) / digest_key\n", "original_content = file_path.read_bytes()\n", "\n", "# Simulate an attacker appending malicious code or modifying the data\n", "tampered_content = b\"malicious_payload\" + original_content\n", "file_path.write_bytes(tampered_content)\n", "\n", "try:\n", " storage.load(digest_key)\n", "except KeyError as e:\n", " print(\"Security Error Caught:\", e)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Key Rotation\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-03-22T19:56:48.605820Z", "iopub.status.busy": "2026-03-22T19:56:48.605622Z", "iopub.status.idle": "2026-03-22T19:56:48.609866Z", "shell.execute_reply": "2026-03-22T19:56:48.609104Z" } }, "outputs": [], "source": "# We roll out a new key, but keep the old one for reading\nos.environ[\"FLECHE_SECRET_KEY\"] = \"aabbccdd0011223344556677aabbccdd0011223344556677aabbccdd00112233:deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef0123\"\nstorage_rotated = PickleFile.with_pickle(root=temp_dir.name)\n\n# Restore the original (untampered) file to test reading with the old key\nfile_path.write_bytes(original_content)\n\n# It successfully loads using the second key in the list!\nrotated_loaded_value = storage_rotated.load(digest_key)\nprint(\"Loaded with rotated key:\", rotated_loaded_value)" } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.13" } }, "nbformat": 4, "nbformat_minor": 4 }