Thread-safe secrets manager with Fernet encryption, multi-version key rotation, and PBKDF2 key derivation (no third-party time dependency).
- Fernet Encryption: AES-128 CBC + HMAC
- Key Rotation: multi-version key management with fallback
- PBKDF2 Derivation: key derivation from passwords
- Salt Integrity: optional SHA256 validation
- Thread-Safe: RLock-protected caches
- Auditability: optional audit callbacks
- Statistics: basic metrics tracking
- Environment Persistence: save/load keys from
.envfiles
Makefiletargets Linux/macOS.- On Windows, use
Makefile.windows:make -f Makefile.windows install-dev make -f Makefile.windows test make -f Makefile.windows check
uv add SecretsManageruv sync --extra devfrom secrets_manager import SecretsConfig, SecretsManager
config = SecretsConfig(
keys={
"v1": {
"key": "my-secret-password",
"salt": b"random-salt-value",
}
},
active_version="v1",
)
manager = SecretsManager(config)
version, ciphertext = manager.encrypt(b"sensitive data")
version, plaintext = manager.decrypt(ciphertext)from secrets_manager import SecretsConfig, SecretsManager
config = SecretsConfig(
keys={"v1": {"key": "old-password", "salt": b"old-salt"}},
active_version="v1",
)
manager = SecretsManager(config)
_, ciphertext_v1 = manager.encrypt(b"important data")
manager.rotate_to_new_version(
new_version="v2",
new_key="new-password",
new_salt=b"new-salt",
)
_, ciphertext_v2 = manager.encrypt(b"new data")
manager.decrypt(ciphertext_v1)
manager.decrypt(ciphertext_v2)import hashlib
salt = b"my-salt"
salt_hash = hashlib.sha256(salt).hexdigest()
config = SecretsConfig(
keys={"v1": {"key": "password", "salt": salt, "salt_hash": salt_hash}},
active_version="v1",
verify_salt_integrity=True,
)def audit_callback(event: str, metadata: dict):
print(f"[AUDIT] {event}: {metadata}")
config = SecretsConfig(
keys={"v1": {"key": "pass", "salt": b"salt"}},
active_version="v1",
audit_callback=audit_callback,
)stats = manager.get_statistics()manager.rotate_to_new_version(
new_version="v2",
new_key="new-key",
new_salt=b"new-salt",
persist_to_file=".env.secrets",
)from secrets_manager import SecretsConfig, SecretsManager
config = SecretsConfig(
keys={"v1": {"key": "my-key", "salt": b"my-salt"}},
active_version="v1",
)
config.to_file(".env.secrets")
loaded = SecretsConfig.from_file(".env.secrets")
manager = SecretsManager(loaded)Note: salt should be passed as bytes in code. When persisted to .env, salts are stored
in base64; from_file()/from_environment() normalize them back to bytes.
Example (visual):
config = SecretsConfig(keys={"v1": {"key": "k", "salt": b"my-salt"}}, active_version="v1")ENCRYPTION_SALT__v1="bXktc2FsdA=="
See examples/env_file_usage.py for a complete runnable example.
.envfiles store keys in plain text; do not version them in git.- Prefer environment variables for production deployments.
- Rotate keys regularly and keep secure backups of older versions for fallback..
from_file()usespython-dotenvfor robust parsing of quoted values and#inside values.to_file(append=True)uses a best-effort file lock; it is not guaranteed to be thread-safe or process-safe on all filesystems.to_file()writesENCRYPTION_ENV_CHECKSUM;from_file()validates it when present.
@dataclass
class SecretsConfig:
keys: Dict[str, Dict[str, Any]]
active_version: str
kdf_iterations: int = 100_000
verify_salt_integrity: bool = True
audit_callback: Optional[Callable] = None
logger: Optional[logging.Logger] = None- Python >= 3.11
- cryptography >= 41.0.0
Contributions are welcome. See CONTRIBUTING.md and INSTALLATION_GUIDE.md for local setup and checks.
This project is licensed under the MIT License. See LICENSE.
Daniel Correa Lobato
- Website: sites.lobato.org
- Email: daniel@lobato.org