Secure Memory - Steel-SecAdv-LLC/AMA-Cryptography GitHub Wiki
Secure Memory
Documentation for the secure memory operations module (ama_cryptography/secure_memory.py), covering SecureBuffer, memory zeroing, memory locking, and constant-time comparisons.
Overview
Cryptographic key material must be handled with care:
- Zeroed when no longer needed (prevent exposure via heap dumps, core files, swap)
- Locked in RAM (prevent exposure via swap/hibernate)
- Compared in constant time (prevent timing side-channel attacks)
The secure_memory module provides these capabilities using only the standard library, with optional acceleration via the AMA native C backend.
SecureBuffer
A context manager for automatic, guaranteed memory zeroing:
from ama_cryptography.secure_memory import SecureBuffer
import os
# Allocate a 32-byte secure buffer
with SecureBuffer(32) as buf:
# buf is a bytearray, initially zeroed
buf[:] = os.urandom(32) # Load key material
# Use buf for cryptographic operations...
key = bytes(buf)
# On context manager __exit__, buf is automatically zeroed
# (multi-pass overwrite, ensuring the key cannot be recovered)
Internal Design
SecureBuffer.__enter__() returns a bytearray (not bytes) because bytearray supports in-place modification, allowing the buffer to be zeroed without creating new memory allocations. bytes objects in Python are immutable and cannot be zeroed in place.
secure_memzero()
Multi-pass memory overwrite for sensitive data:
from ama_cryptography.secure_memory import secure_memzero
# Load sensitive data
secret_key = bytearray(os.urandom(32))
# Use the key...
signature = sign(message, bytes(secret_key))
# Zero immediately after use
secure_memzero(secret_key)
# secret_key is now all zeros
Important: Always pass a
bytearray, notbytes.bytesobjects are immutable and cannot be zeroed.
Implementation
secure_memzero() performs multiple overwrite passes:
- Fill with
0x00 - Fill with
0xFF - Final fill with
0x00
This protects against "compiler optimization" attacks where a naive memset() call to zero is optimized away because the buffer is not subsequently read.
secure_mlock() and secure_munlock()
Lock memory pages to prevent swapping to disk:
from ama_cryptography.secure_memory import secure_mlock, secure_munlock
secret = bytearray(os.urandom(32))
# Lock the memory page containing `secret` into RAM
# Raises NotImplementedError if native C or POSIX mlock unavailable
secure_mlock(secret)
print("Memory locked in RAM (will not swap)")
# ... use secret ...
# Unlock when done (allow the OS to swap it again)
secure_munlock(secret)
# Zero the memory
secure_memzero(secret)
Platform Notes
| Platform | API | Notes |
|---|---|---|
| Linux | mlock() |
Requires CAP_IPC_LOCK or ulimit -l ≥ buffer size |
| macOS | mlock() |
Requires entitlements in sandboxed environments |
| Windows | VirtualLock() |
Standard user processes have limits |
Native C backend: If the AMA native C library is available,
secure_mlock()delegates toama_secure_mlock(). Otherwise falls back to POSIXmlock().
constant_time_compare()
Timing-safe byte comparison:
from ama_cryptography.secure_memory import constant_time_compare
# Timing-safe comparison (always runs in O(n) time regardless of where mismatch occurs)
hmac_expected = compute_hmac(message, key)
hmac_received = package["hmac_tag"]
# Safe: does not leak position of first mismatch
if constant_time_compare(hmac_expected, hmac_received):
print("HMAC valid")
else:
print("HMAC invalid")
Why Constant-Time Matters
A naive comparison (expected == received) returns False as soon as it finds the first differing byte. An attacker making many requests can measure small timing differences to determine byte-by-byte what the correct HMAC tag is (a timing oracle attack).
constant_time_compare() always processes all bytes in equal time, preventing this.
Implementation
Uses ama_consttime_memcmp() from AMA's native C library when available, with a fallback to a pure-Python XOR accumulator that pads both inputs to equal length and never short-circuits.
SecureKeyStorage
Encrypted storage with automatic memory management:
from ama_cryptography.key_management import SecureKeyStorage
# encryption_key is bytearray to allow in-place zeroing
encryption_key = bytearray(os.urandom(32))
with SecureKeyStorage(encryption_key) as storage:
# Store key material (encrypted with AES-256-GCM)
storage.store("signing-key-v1", os.urandom(32))
# Retrieve key material (decrypted on access)
key = storage.retrieve("signing-key-v1")
# encryption_key is zeroed on context manager exit
Best Practices
Do: Use bytearray for Key Material
# ✓ Correct: bytearray can be zeroed
key = bytearray(os.urandom(32))
# ... use key ...
secure_memzero(key)
# ✗ Incorrect: bytes cannot be zeroed in-place
key = os.urandom(32) # bytes object
# Cannot zero this after use
Do: Use SecureBuffer Context Manager
# ✓ Automatic zeroing even on exception
with SecureBuffer(32) as buf:
buf[:] = get_key_from_hsm()
result = encrypt(plaintext, bytes(buf))
# buf is zeroed here, even if encrypt() raised
Do: Lock Sensitive Buffers in RAM
# ✓ Prevent swap exposure for long-lived keys
master_key = bytearray(load_master_key())
secure_mlock(master_key)
# ... use master_key for the session ...
secure_munlock(master_key)
secure_memzero(master_key)
Do: Use Constant-Time Comparisons for MACs and Secrets
# ✓ Constant-time comparison for HMAC tags
if constant_time_compare(expected_mac, received_mac):
proceed()
# ✗ Timing-vulnerable comparison
if expected_mac == received_mac: # DO NOT USE for secrets
proceed()
Native C Backend
When the AMA native C library is available (built via CMake), the secure memory module uses it for:
secure_mlock()/secure_munlock()viaama_secure_mlock()/ama_secure_munlock()constant_time_compare()viaama_consttime_memcmp()
Without the native C library, POSIX mlock()/munlock() and a pure-Python XOR accumulator are used as fallbacks.
See Key Management for how SecureBuffer is used in key storage, or Architecture for the security architecture overview.