feat: enhance security by adding security_tier parameter and improving secure memory handling

This commit introduces the security_tier parameter to all SecureChatCompletion calls, allowing users to specify security levels (standard, high, maximum). It also refactors secure memory management in SecureCompletionClient.py to use secure_bytearray instead of secure_bytes, improves memory protection by aligning addresses to page boundaries, and enhances the secure memory locking mechanism. The README.md has been updated to document the new security_tier parameter and include missing import statements.
This commit is contained in:
Alpha Nerd 2026-02-03 13:59:46 +01:00
parent 2fae7d1d24
commit 5641a746b7
4 changed files with 509 additions and 127 deletions

View file

@ -30,6 +30,7 @@ async def main():
messages=[ messages=[
{"role": "user", "content": "Hello! How are you today?"} {"role": "user", "content": "Hello! How are you today?"}
], ],
security_tier="standard", #optional: standard, high or maximum
temperature=0.7 temperature=0.7
) )
@ -156,6 +157,7 @@ async def main():
{"role": "system", "content": "You are a helpful assistant."}, {"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"} {"role": "user", "content": "What is the capital of France?"}
], ],
security_tier="standard", #optional: standard, high or maximum
temperature=0.7 temperature=0.7
) )
@ -167,6 +169,7 @@ asyncio.run(main())
### With Tools ### With Tools
```python ```python
import asyncio import asyncio
from nomyo import SecureChatCompletion from nomyo import SecureChatCompletion
@ -194,6 +197,7 @@ async def main():
} }
} }
], ],
security_tier="standard", #optional: standard, high or maximum
temperature=0.7 temperature=0.7
) )

View file

@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
# Import secure memory module # Import secure memory module
try: try:
from .SecureMemory import secure_bytes, get_memory_protection_info from .SecureMemory import secure_bytearray, get_memory_protection_info, _get_secure_memory
_SECURE_MEMORY_AVAILABLE = True _SECURE_MEMORY_AVAILABLE = True
except ImportError: except ImportError:
_SECURE_MEMORY_AVAILABLE = False _SECURE_MEMORY_AVAILABLE = False
@ -115,9 +115,9 @@ class SecureCompletionClient:
# Note: This is best-effort as the cryptography library # Note: This is best-effort as the cryptography library
# maintains its own internal key material # maintains its own internal key material
import pickle import pickle
key_data = pickle.dumps(self.private_key) key_data = bytearray(pickle.dumps(self.private_key))
from .SecureMemory import _secure_memory secure_memory = _get_secure_memory()
locked = _secure_memory.lock_memory(key_data) locked = secure_memory.lock_memory(key_data)
if locked: if locked:
logger.debug("Private key locked in memory (best effort)") logger.debug("Private key locked in memory (best effort)")
else: else:
@ -369,22 +369,22 @@ class SecureCompletionClient:
# Use secure memory context to protect plaintext payload # Use secure memory context to protect plaintext payload
if _SECURE_MEMORY_AVAILABLE: if _SECURE_MEMORY_AVAILABLE:
with secure_bytes(payload_json) as protected_payload: with secure_bytearray(payload_json) as protected_payload:
# Generate cryptographically secure random AES key # Generate cryptographically secure random AES key
aes_key = secrets.token_bytes(32) # 256-bit key aes_key = secrets.token_bytes(32) # 256-bit key
try: try:
# Protect AES key in memory # Protect AES key in memory
with secure_bytes(bytearray(aes_key)) as protected_aes_key: with secure_bytearray(aes_key) as protected_aes_key:
# Encrypt payload with AES-GCM using Cipher API # Encrypt payload with AES-GCM using Cipher API
nonce = secrets.token_bytes(12) # 96-bit nonce for GCM nonce = secrets.token_bytes(12) # 96-bit nonce for GCM
cipher = Cipher( cipher = Cipher(
algorithms.AES(bytes(protected_aes_key)), algorithms.AES(bytes(protected_aes_key.data)),
modes.GCM(nonce), modes.GCM(nonce),
backend=default_backend() backend=default_backend()
) )
encryptor = cipher.encryptor() encryptor = cipher.encryptor()
ciphertext = encryptor.update(protected_payload) + encryptor.finalize() ciphertext = encryptor.update(bytes(protected_payload.data)) + encryptor.finalize()
tag = encryptor.tag tag = encryptor.tag
# Fetch server's public key for encrypting the AES key # Fetch server's public key for encrypting the AES key
@ -396,7 +396,7 @@ class SecureCompletionClient:
backend=default_backend() backend=default_backend()
) )
encrypted_aes_key = server_public_key.encrypt( encrypted_aes_key = server_public_key.encrypt(
bytes(protected_aes_key), bytes(protected_aes_key.data),
padding.OAEP( padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()), mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(), algorithm=hashes.SHA256(),
@ -556,14 +556,14 @@ class SecureCompletionClient:
# Use secure memory to protect AES key and decrypted plaintext # Use secure memory to protect AES key and decrypted plaintext
if _SECURE_MEMORY_AVAILABLE: if _SECURE_MEMORY_AVAILABLE:
# Protect AES key in memory # Protect AES key in memory
with secure_bytes(bytearray(aes_key)) as protected_aes_key: with secure_bytearray(aes_key) as protected_aes_key:
# Decrypt payload with AES-GCM using Cipher API # Decrypt payload with AES-GCM using Cipher API
ciphertext = base64.b64decode(package["encrypted_payload"]["ciphertext"]) ciphertext = base64.b64decode(package["encrypted_payload"]["ciphertext"])
nonce = base64.b64decode(package["encrypted_payload"]["nonce"]) nonce = base64.b64decode(package["encrypted_payload"]["nonce"])
tag = base64.b64decode(package["encrypted_payload"]["tag"]) tag = base64.b64decode(package["encrypted_payload"]["tag"])
cipher = Cipher( cipher = Cipher(
algorithms.AES(bytes(protected_aes_key)), algorithms.AES(bytes(protected_aes_key.data)),
modes.GCM(nonce, tag), modes.GCM(nonce, tag),
backend=default_backend() backend=default_backend()
) )
@ -571,9 +571,9 @@ class SecureCompletionClient:
plaintext = decryptor.update(ciphertext) + decryptor.finalize() plaintext = decryptor.update(ciphertext) + decryptor.finalize()
# Protect decrypted plaintext in memory # Protect decrypted plaintext in memory
with secure_bytes(bytearray(plaintext)) as protected_plaintext: with secure_bytearray(plaintext) as protected_plaintext:
# Parse decrypted response # Parse decrypted response
response = json.loads(bytes(protected_plaintext).decode('utf-8')) response = json.loads(bytes(protected_plaintext.data).decode('utf-8'))
# Plaintext automatically zeroed here # Plaintext automatically zeroed here
# AES key automatically zeroed here # AES key automatically zeroed here

View file

@ -7,7 +7,7 @@ usage with Python's memory management characteristics.
Supports: Supports:
- Linux: mlock() + memset() - Linux: mlock() + memset()
- Windows: VirtualLock() + RtlSecureZeroMemory() - Windows: VirtualLock() + RtlZeroMemory()
- macOS: mlock() + memset() - macOS: mlock() + memset()
- Fallback: ctypes-based zeroing for unsupported platforms - Fallback: ctypes-based zeroing for unsupported platforms
@ -16,34 +16,137 @@ Security Features:
- Guarantees memory is zeroed before deallocation - Guarantees memory is zeroed before deallocation
- Context managers for automatic cleanup - Context managers for automatic cleanup
- No root privileges required (uses capabilities on Linux) - No root privileges required (uses capabilities on Linux)
IMPORTANT: This module works with mutable bytearray objects for true security.
Python's immutable bytes objects cannot be securely zeroed in place.
""" """
import os
import sys
import ctypes import ctypes
import logging import logging
from typing import Optional, Any import sys
from contextlib import contextmanager from contextlib import contextmanager
from enum import Enum from enum import Enum
from typing import Optional, Union
# Configure logging # Configure logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class MemoryProtectionLevel(Enum): class MemoryProtectionLevel(Enum):
"""Memory protection levels available""" """Memory protection levels available"""
NONE = "none" # No protection (fallback only) NONE = "none" # No protection (fallback only)
ZEROING_ONLY = "zeroing_only" # Memory zeroing without locking ZEROING_ONLY = "zeroing_only" # Memory zeroing without locking
FULL = "full" # Memory locking + zeroing FULL = "full" # Memory locking + zeroing
class SecureBuffer:
"""
A secure buffer that wraps a bytearray with proper memory protection.
This class provides:
- Correct memory address calculation using ctypes
- Platform-specific memory locking
- Guaranteed secure zeroing on cleanup
- Context manager support for automatic cleanup
Usage:
with SecureBuffer(secret_data) as buf:
# Use buf.data (bytearray) for operations
process(buf.data)
# Memory is securely zeroed here
"""
def __init__(self, data: Union[bytes, bytearray], secure_memory: 'SecureMemory'):
"""
Initialize a secure buffer.
Args:
data: Initial data (will be copied into a mutable bytearray)
secure_memory: SecureMemory instance for platform operations
"""
self._secure_memory = secure_memory
self._locked = False
self._size = len(data)
# Create mutable bytearray and ctypes buffer for proper address handling
self._data = bytearray(data)
self._ctypes_buffer = (ctypes.c_char * self._size).from_buffer(self._data)
self._address = ctypes.addressof(self._ctypes_buffer)
# Zero the original if it was a bytearray (caller's copy)
if isinstance(data, bytearray):
for i in range(len(data)):
data[i] = 0
@property
def data(self) -> bytearray:
"""Get the underlying bytearray data."""
return self._data
@property
def address(self) -> int:
"""Get the memory address of the buffer."""
return self._address
@property
def size(self) -> int:
"""Get the size of the buffer."""
return self._size
def lock(self) -> bool:
"""Lock the buffer memory to prevent swapping."""
if self._locked or not self._secure_memory.enabled:
return self._locked
self._locked = self._secure_memory._lock_memory_at(self._address, self._size)
return self._locked
def unlock(self) -> bool:
"""Unlock the buffer memory."""
if not self._locked:
return True
result = self._secure_memory._unlock_memory_at(self._address, self._size)
if result:
self._locked = False
return result
def zero(self) -> None:
"""Securely zero the buffer contents."""
self._secure_memory._zero_memory_at(self._address, self._size)
# Also zero the Python bytearray for defense in depth
for i in range(len(self._data)):
self._data[i] = 0
def __enter__(self) -> 'SecureBuffer':
self.lock()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.zero()
self.unlock()
def __len__(self) -> int:
return self._size
def __bytes__(self) -> bytes:
"""Convert to bytes (creates a copy - use with caution)."""
return bytes(self._data)
class SecureMemory: class SecureMemory:
""" """
Cross-platform secure memory handler for client-side. Cross-platform secure memory handler for client-side.
Automatically detects platform and provides best-available security: Automatically detects platform and provides best-available security:
- Linux: mlock + memset - Linux: mlock + memset
- Windows: VirtualLock + RtlSecureZeroMemory - Windows: VirtualLock + RtlZeroMemory
- macOS: mlock + memset - macOS: mlock + memset
- Others: Fallback zeroing - Others: Fallback zeroing
IMPORTANT: For true security, use SecureBuffer or secure_bytearray() context
manager with bytearray objects. Python's immutable bytes cannot be securely
zeroed in place.
""" """
def __init__(self, enable: bool = True): def __init__(self, enable: bool = True):
@ -59,6 +162,13 @@ class SecureMemory:
self.has_mlock = False self.has_mlock = False
self.has_secure_zero = False self.has_secure_zero = False
self.protection_level = MemoryProtectionLevel.NONE self.protection_level = MemoryProtectionLevel.NONE
self._page_size = 4096 # Default, will be updated per platform
# Platform-specific function references
self._mlock_func = None
self._munlock_func = None
self._memset_func = None
self._libc = None
if self.enabled: if self.enabled:
self._init_platform_specific() self._init_platform_specific()
@ -84,14 +194,28 @@ class SecureMemory:
def _init_linux(self): def _init_linux(self):
"""Initialize Linux-specific functions (mlock + memset)""" """Initialize Linux-specific functions (mlock + memset)"""
try: try:
self.libc = ctypes.CDLL('libc.so.6') # Use use_errno=True for proper errno handling
self.mlock = self.libc.mlock self._libc = ctypes.CDLL('libc.so.6', use_errno=True)
self.munlock = self.libc.munlock
self.memset = self.libc.memset
# Set return types # Get page size
self.mlock.restype = ctypes.c_int try:
self.munlock.restype = ctypes.c_int self._page_size = self._libc.getpagesize()
except Exception:
self._page_size = 4096
# Setup mlock/munlock
self._mlock_func = self._libc.mlock
self._mlock_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._mlock_func.restype = ctypes.c_int
self._munlock_func = self._libc.munlock
self._munlock_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._munlock_func.restype = ctypes.c_int
# Setup memset for secure zeroing
self._memset_func = self._libc.memset
self._memset_func.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_size_t]
self._memset_func.restype = ctypes.c_void_p
self.has_mlock = True self.has_mlock = True
self.has_secure_zero = True self.has_secure_zero = True
@ -104,24 +228,69 @@ class SecureMemory:
self._init_fallback() self._init_fallback()
def _init_windows(self): def _init_windows(self):
"""Initialize Windows-specific functions (VirtualLock + RtlSecureZeroMemory)""" """Initialize Windows-specific functions (VirtualLock + RtlZeroMemory)"""
try: try:
kernel32 = ctypes.windll.kernel32 kernel32 = ctypes.windll.kernel32
# VirtualLock for memory locking # Get page size
self.virtual_lock = kernel32.VirtualLock class SYSTEM_INFO(ctypes.Structure):
self.virtual_unlock = kernel32.VirtualUnlock _fields_ = [
self.virtual_lock.restype = ctypes.c_bool ("wProcessorArchitecture", ctypes.c_ushort),
self.virtual_unlock.restype = ctypes.c_bool ("wReserved", ctypes.c_ushort),
("dwPageSize", ctypes.c_ulong),
("lpMinimumApplicationAddress", ctypes.c_void_p),
("lpMaximumApplicationAddress", ctypes.c_void_p),
("dwActiveProcessorMask", ctypes.c_void_p),
("dwNumberOfProcessors", ctypes.c_ulong),
("dwProcessorType", ctypes.c_ulong),
("dwAllocationGranularity", ctypes.c_ulong),
("wProcessorLevel", ctypes.c_ushort),
("wProcessorRevision", ctypes.c_ushort),
]
# RtlSecureZeroMemory for guaranteed zeroing try:
self.secure_zero_memory = kernel32.RtlSecureZeroMemory sysinfo = SYSTEM_INFO()
kernel32.GetSystemInfo(ctypes.byref(sysinfo))
self._page_size = sysinfo.dwPageSize
except Exception:
self._page_size = 4096
# VirtualLock for memory locking
self._mlock_func = kernel32.VirtualLock
self._mlock_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._mlock_func.restype = ctypes.c_bool
self._munlock_func = kernel32.VirtualUnlock
self._munlock_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._munlock_func.restype = ctypes.c_bool
# RtlZeroMemory from ntdll (not RtlSecureZeroMemory which is a macro)
# Note: RtlZeroMemory may be optimized away by compiler, but it's the
# best we can do from Python. For true secure zeroing, we also
# implement a Python-level volatile write pattern.
try:
ntdll = ctypes.windll.ntdll
self._memset_func = ntdll.RtlZeroMemory
self._memset_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._memset_func.restype = None
self._windows_zero_is_rtlzero = True
except Exception:
# Fallback to kernel32.RtlZeroMemory if available
try:
self._memset_func = kernel32.RtlZeroMemory
self._memset_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._memset_func.restype = None
self._windows_zero_is_rtlzero = True
except Exception:
self._memset_func = None
self._windows_zero_is_rtlzero = False
logger.warning("RtlZeroMemory not available, using Python zeroing")
self.has_mlock = True self.has_mlock = True
self.has_secure_zero = True self.has_secure_zero = True # We have fallback even if RtlZeroMemory fails
self.protection_level = MemoryProtectionLevel.FULL self.protection_level = MemoryProtectionLevel.FULL
logger.info("Windows secure memory initialized (VirtualLock + RtlSecureZeroMemory)") logger.info("Windows secure memory initialized (VirtualLock + RtlZeroMemory)")
except Exception as e: except Exception as e:
logger.warning(f"Could not initialize Windows VirtualLock: {e}. Using fallback.") logger.warning(f"Could not initialize Windows VirtualLock: {e}. Using fallback.")
@ -130,14 +299,28 @@ class SecureMemory:
def _init_macos(self): def _init_macos(self):
"""Initialize macOS-specific functions (mlock + memset)""" """Initialize macOS-specific functions (mlock + memset)"""
try: try:
self.libc = ctypes.CDLL('libc.dylib') # Use use_errno=True for proper errno handling
self.mlock = self.libc.mlock self._libc = ctypes.CDLL('libc.dylib', use_errno=True)
self.munlock = self.libc.munlock
self.memset = self.libc.memset
# Set return types # Get page size
self.mlock.restype = ctypes.c_int try:
self.munlock.restype = ctypes.c_int self._page_size = self._libc.getpagesize()
except Exception:
self._page_size = 4096
# Setup mlock/munlock
self._mlock_func = self._libc.mlock
self._mlock_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._mlock_func.restype = ctypes.c_int
self._munlock_func = self._libc.munlock
self._munlock_func.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self._munlock_func.restype = ctypes.c_int
# Setup memset for secure zeroing
self._memset_func = self._libc.memset
self._memset_func.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_size_t]
self._memset_func.restype = ctypes.c_void_p
self.has_mlock = True self.has_mlock = True
self.has_secure_zero = True self.has_secure_zero = True
@ -152,8 +335,11 @@ class SecureMemory:
def _init_fallback(self): def _init_fallback(self):
"""Initialize fallback memory zeroing (no locking)""" """Initialize fallback memory zeroing (no locking)"""
self.has_mlock = False self.has_mlock = False
self.has_secure_zero = False self.has_secure_zero = True # We can still zero memory at Python level
self.protection_level = MemoryProtectionLevel.ZEROING_ONLY self.protection_level = MemoryProtectionLevel.ZEROING_ONLY
self._mlock_func = None
self._munlock_func = None
self._memset_func = None
logger.info("Using fallback memory protection (zeroing only, no locking)") logger.info("Using fallback memory protection (zeroing only, no locking)")
def _log_capabilities(self): def _log_capabilities(self):
@ -163,61 +349,80 @@ class SecureMemory:
f"Platform: {self.platform}, " f"Platform: {self.platform}, "
f"Protection Level: {self.protection_level.value}, " f"Protection Level: {self.protection_level.value}, "
f"Memory Locking: {self.has_mlock}, " f"Memory Locking: {self.has_mlock}, "
f"Secure Zeroing: {self.has_secure_zero}" f"Secure Zeroing: {self.has_secure_zero}, "
f"Page Size: {self._page_size}"
) )
def lock_memory(self, data: bytes) -> bool: def _get_page_aligned_range(self, addr: int, size: int) -> tuple:
""" """
Lock memory pages containing data to prevent swapping to disk. Calculate page-aligned address and size for mlock operations.
Args: Args:
data: Bytes object to lock in memory addr: Memory address
size: Size in bytes
Returns:
Tuple of (aligned_addr, aligned_size)
"""
page_mask = self._page_size - 1
aligned_addr = addr & ~page_mask
end_addr = addr + size
aligned_end = (end_addr + page_mask) & ~page_mask
aligned_size = aligned_end - aligned_addr
return aligned_addr, aligned_size
def _lock_memory_at(self, addr: int, size: int) -> bool:
"""
Lock memory at a specific address.
Args:
addr: Memory address (will be page-aligned)
size: Size in bytes
Returns: Returns:
True if successfully locked, False otherwise True if successfully locked, False otherwise
""" """
if not self.enabled or not self.has_mlock or not data: if not self.enabled or not self.has_mlock or not self._mlock_func:
return False return False
try: try:
# Get memory address and size # Page-align the address and size
addr = id(data) aligned_addr, aligned_size = self._get_page_aligned_range(addr, size)
size = len(data)
if self.platform.startswith('linux') or self.platform == 'darwin': if self.platform.startswith('linux') or self.platform == 'darwin':
# POSIX mlock result = self._mlock_func(
result = self.mlock( ctypes.c_void_p(aligned_addr),
ctypes.c_void_p(addr), ctypes.c_size_t(aligned_size)
ctypes.c_size_t(size)
) )
if result != 0: if result != 0:
errno = ctypes.get_errno() errno = ctypes.get_errno()
# ENOMEM (12) or EPERM (1) are common errors if errno == 1: # EPERM
if errno == 1:
logger.debug( logger.debug(
"mlock permission denied. " "mlock permission denied. "
"Grant CAP_IPC_LOCK or increase ulimit -l" "Grant CAP_IPC_LOCK or increase ulimit -l"
) )
elif errno == 12: elif errno == 12: # ENOMEM
logger.debug("mlock failed: insufficient memory or limit exceeded") logger.debug("mlock failed: insufficient memory or limit exceeded")
else: else:
logger.debug(f"mlock failed with errno {errno}") logger.debug(f"mlock failed with errno {errno}")
return False return False
logger.debug(f"Memory locked: {size} bytes at 0x{addr:x}")
return True return True
elif self.platform == 'win32': elif self.platform == 'win32':
# Windows VirtualLock result = self._mlock_func(
result = self.virtual_lock( ctypes.c_void_p(aligned_addr),
ctypes.c_void_p(addr), ctypes.c_size_t(aligned_size)
ctypes.c_size_t(size)
) )
if not result: if not result:
logger.debug("VirtualLock failed") error = ctypes.get_last_error()
logger.debug(f"VirtualLock failed with error {error}")
return False return False
logger.debug(f"Memory locked: {size} bytes at 0x{addr:x}")
return True return True
except Exception as e: except Exception as e:
@ -226,37 +431,41 @@ class SecureMemory:
return False return False
def unlock_memory(self, data: bytes) -> bool: def _unlock_memory_at(self, addr: int, size: int) -> bool:
""" """
Unlock previously locked memory pages. Unlock memory at a specific address.
Args: Args:
data: Bytes object to unlock addr: Memory address (will be page-aligned)
size: Size in bytes
Returns: Returns:
True if successfully unlocked, False otherwise True if successfully unlocked, False otherwise
""" """
if not self.enabled or not self.has_mlock or not data: if not self.enabled or not self.has_mlock or not self._munlock_func:
return False return False
try: try:
addr = id(data) # Page-align the address and size
size = len(data) aligned_addr, aligned_size = self._get_page_aligned_range(addr, size)
if self.platform.startswith('linux') or self.platform == 'darwin': if self.platform.startswith('linux') or self.platform == 'darwin':
# POSIX munlock result = self._munlock_func(
result = self.munlock( ctypes.c_void_p(aligned_addr),
ctypes.c_void_p(addr), ctypes.c_size_t(aligned_size)
ctypes.c_size_t(size)
) )
return result == 0 success = result == 0
if success:
logger.debug(f"Memory unlocked: {size} bytes at 0x{addr:x}")
return success
elif self.platform == 'win32': elif self.platform == 'win32':
# Windows VirtualUnlock result = self._munlock_func(
result = self.virtual_unlock( ctypes.c_void_p(aligned_addr),
ctypes.c_void_p(addr), ctypes.c_size_t(aligned_size)
ctypes.c_size_t(size)
) )
if result:
logger.debug(f"Memory unlocked: {size} bytes at 0x{addr:x}")
return bool(result) return bool(result)
except Exception as e: except Exception as e:
@ -265,39 +474,166 @@ class SecureMemory:
return False return False
def zero_memory(self, data: bytes) -> None: def _zero_memory_at(self, addr: int, size: int) -> None:
""" """
Securely zero memory contents. Securely zero memory at a specific address.
Note: Due to Python's memory management, we cannot directly zero Uses platform-specific functions when available, with Python fallback.
immutable bytes objects. This function is a best-effort approach
that works better with mutable bytearray objects.
For maximum security, use this with bytearray instead of bytes,
or rely on memory locking to prevent swapping.
Args: Args:
data: Bytes object to zero (best effort for bytes, effective for bytearray) addr: Memory address
size: Size in bytes
"""
if not self.enabled or size == 0:
return
try:
if self._memset_func is not None:
if self.platform.startswith('linux') or self.platform == 'darwin':
# memset(addr, 0, size)
self._memset_func(
ctypes.c_void_p(addr),
ctypes.c_int(0),
ctypes.c_size_t(size)
)
elif self.platform == 'win32' and hasattr(self, '_windows_zero_is_rtlzero'):
# RtlZeroMemory(addr, size)
self._memset_func(
ctypes.c_void_p(addr),
ctypes.c_size_t(size)
)
logger.debug(f"Memory zeroed (native): {size} bytes at 0x{addr:x}")
else:
# Fallback: zero via ctypes byte-by-byte
# This is slower but works everywhere
char_array = (ctypes.c_char * size).from_address(addr)
for i in range(size):
char_array[i] = b'\x00'
logger.debug(f"Memory zeroed (fallback): {size} bytes at 0x{addr:x}")
except Exception as e:
logger.warning(f"Memory zeroing failed: {e}")
# Last resort: try to zero via ctypes
try:
char_array = (ctypes.c_char * size).from_address(addr)
for i in range(size):
char_array[i] = b'\x00'
except Exception:
pass
def create_secure_buffer(self, data: Union[bytes, bytearray]) -> SecureBuffer:
"""
Create a SecureBuffer from data.
This is the recommended way to handle sensitive data.
Args:
data: Data to protect (will be copied, original should be discarded)
Returns:
SecureBuffer instance
"""
return SecureBuffer(data, self)
def lock_memory(self, data: bytearray) -> bool:
"""
Lock memory containing a bytearray to prevent swapping.
IMPORTANT: Only works reliably with bytearray objects.
Use create_secure_buffer() for better security guarantees.
Args:
data: Bytearray to lock in memory
Returns:
True if successfully locked, False otherwise
"""
if not isinstance(data, bytearray):
logger.warning(
"lock_memory() called with non-bytearray. "
"Use create_secure_buffer() for bytes objects."
)
return False
if not self.enabled or not self.has_mlock or not data:
return False
try:
# Create ctypes buffer to get correct address
ctypes_buffer = (ctypes.c_char * len(data)).from_buffer(data)
addr = ctypes.addressof(ctypes_buffer)
return self._lock_memory_at(addr, len(data))
except Exception as e:
logger.debug(f"Memory lock failed: {e}")
return False
def unlock_memory(self, data: bytearray) -> bool:
"""
Unlock previously locked bytearray memory.
Args:
data: Bytearray to unlock
Returns:
True if successfully unlocked, False otherwise
"""
if not isinstance(data, bytearray):
return False
if not self.enabled or not self.has_mlock or not data:
return False
try:
ctypes_buffer = (ctypes.c_char * len(data)).from_buffer(data)
addr = ctypes.addressof(ctypes_buffer)
return self._unlock_memory_at(addr, len(data))
except Exception as e:
logger.debug(f"Memory unlock failed: {e}")
return False
def zero_memory(self, data: bytearray) -> None:
"""
Securely zero a bytearray's memory contents.
IMPORTANT: Only works with mutable bytearray objects.
Python's immutable bytes cannot be securely zeroed.
Args:
data: Bytearray to zero
""" """
if not self.enabled or not data: if not self.enabled or not data:
return return
if not isinstance(data, bytearray):
logger.warning(
"zero_memory() called with non-bytearray. "
"Python bytes are immutable and cannot be securely zeroed. "
"Use bytearray or create_secure_buffer() instead."
)
return
try: try:
# For bytearray (mutable), we can zero it # Get correct address via ctypes
if isinstance(data, bytearray): ctypes_buffer = (ctypes.c_char * len(data)).from_buffer(data)
# Zero the bytearray in place addr = ctypes.addressof(ctypes_buffer)
# Use platform-specific zeroing
self._zero_memory_at(addr, len(data))
# Also zero at Python level for defense in depth
for i in range(len(data)): for i in range(len(data)):
data[i] = 0 data[i] = 0
logger.debug(f"Zeroed bytearray: {len(data)} bytes") logger.debug(f"Zeroed bytearray: {len(data)} bytes")
else:
# For bytes (immutable), we can't actually zero the memory
# Python's bytes are immutable and reference counted
# The best we can do is ensure no lingering references
# and let Python's GC handle it
logger.debug(f"Bytes object is immutable, relying on GC: {len(data)} bytes")
except Exception as e: except Exception as e:
logger.debug(f"Memory zeroing note: {e}") logger.warning(f"Memory zeroing error: {e}")
# Fallback: zero at Python level only
try:
for i in range(len(data)):
data[i] = 0
except Exception:
pass
def get_protection_info(self) -> dict: def get_protection_info(self) -> dict:
""" """
@ -312,34 +648,45 @@ class SecureMemory:
"protection_level": self.protection_level.value, "protection_level": self.protection_level.value,
"has_memory_locking": self.has_mlock, "has_memory_locking": self.has_mlock,
"has_secure_zeroing": self.has_secure_zero, "has_secure_zeroing": self.has_secure_zero,
"supports_full_protection": self.protection_level == MemoryProtectionLevel.FULL "supports_full_protection": self.protection_level == MemoryProtectionLevel.FULL,
"page_size": self._page_size
} }
# Global secure memory instance # Global secure memory instance
_secure_memory = SecureMemory() _secure_memory: Optional[SecureMemory] = None
def _get_secure_memory() -> SecureMemory:
"""Get or create the global SecureMemory instance."""
global _secure_memory
if _secure_memory is None:
_secure_memory = SecureMemory()
return _secure_memory
@contextmanager @contextmanager
def secure_bytes(data: bytes, lock: bool = True): def secure_bytearray(data: Union[bytes, bytearray], lock: bool = True):
""" """
Context manager for secure byte handling with automatic cleanup. Context manager for secure bytearray handling with automatic cleanup.
Provides: Provides:
- Proper memory address handling via ctypes
- Optional memory locking to prevent swapping - Optional memory locking to prevent swapping
- Guaranteed memory zeroing on exit - Guaranteed memory zeroing on exit (both native and Python level)
- Automatic cleanup even if exceptions occur - Automatic cleanup even if exceptions occur
Args: Args:
data: Bytes to protect data: Data to protect (bytes or bytearray, will be converted to bytearray)
lock: Whether to attempt memory locking (default: True) lock: Whether to attempt memory locking (default: True)
Set to False to skip locking but still zero on exit
Yields: Yields:
The protected bytes object SecureBuffer containing the protected data
Example: Example:
payload = json.dumps({"secret": "data"}).encode('utf-8') payload = json.dumps({"secret": "data"}).encode('utf-8')
with secure_bytes(payload) as protected_data: with secure_bytearray(payload) as buf:
encrypted = encrypt(protected_data) encrypted = encrypt(buf.data)
# Use encrypted data... # Use encrypted data...
# Memory automatically zeroed here # Memory automatically zeroed here
@ -347,32 +694,57 @@ def secure_bytes(data: bytes, lock: bool = True):
Memory locking may fail without appropriate privileges. Memory locking may fail without appropriate privileges.
In that case, only zeroing is performed (still provides value). In that case, only zeroing is performed (still provides value).
""" """
locked = False sm = _get_secure_memory()
secure_buf = sm.create_secure_buffer(data)
try: try:
# Try to lock memory if requested if lock:
if lock and _secure_memory.enabled: locked = secure_buf.lock()
locked = _secure_memory.lock_memory(data)
if locked: if locked:
logger.debug(f"Memory locked: {len(data)} bytes") logger.debug(f"Memory locked: {len(secure_buf)} bytes")
else: else:
logger.debug( logger.debug(
f"Memory locking not available for {len(data)} bytes, " f"Memory locking not available for {len(secure_buf)} bytes, "
"using zeroing only" "using zeroing only"
) )
# Yield data for use yield secure_buf
yield data
finally: finally:
# Always zero memory # Always zero memory
_secure_memory.zero_memory(data) secure_buf.zero()
logger.debug(f"Memory zeroed: {len(data)} bytes") logger.debug(f"Memory zeroed: {len(secure_buf)} bytes")
# Unlock if we attempted locking
if lock:
secure_buf.unlock()
logger.debug(f"Memory unlocked: {len(secure_buf)} bytes")
# Legacy API - maintained for backwards compatibility
@contextmanager
def secure_bytes(data: bytes, lock: bool = True):
"""
DEPRECATED: Use secure_bytearray() instead.
This function is maintained for backwards compatibility but provides
weaker security guarantees. Python's immutable bytes cannot be securely
zeroed in place.
Args:
data: Bytes to protect
lock: Whether to attempt memory locking
Yields:
SecureBuffer (use .data attribute for the bytearray)
"""
logger.warning(
"secure_bytes() is deprecated. Use secure_bytearray() instead. "
"The original bytes object cannot be securely zeroed."
)
with secure_bytearray(data, lock) as buf:
yield buf
# Unlock if locked
if locked:
_secure_memory.unlock_memory(data)
logger.debug(f"Memory unlocked: {len(data)} bytes")
def get_memory_protection_info() -> dict: def get_memory_protection_info() -> dict:
""" """
@ -381,7 +753,8 @@ def get_memory_protection_info() -> dict:
Returns: Returns:
Dictionary with platform and capability information Dictionary with platform and capability information
""" """
return _secure_memory.get_protection_info() return _get_secure_memory().get_protection_info()
def disable_secure_memory() -> None: def disable_secure_memory() -> None:
""" """
@ -393,6 +766,7 @@ def disable_secure_memory() -> None:
_secure_memory = SecureMemory(enable=False) _secure_memory = SecureMemory(enable=False)
logger.info("Secure memory operations disabled globally") logger.info("Secure memory operations disabled globally")
def enable_secure_memory() -> None: def enable_secure_memory() -> None:
""" """
Re-enable secure memory operations globally. Re-enable secure memory operations globally.

View file

@ -20,7 +20,9 @@ try:
get_memory_protection_info, get_memory_protection_info,
disable_secure_memory, disable_secure_memory,
enable_secure_memory, enable_secure_memory,
secure_bytes secure_bytearray,
secure_bytes, # Deprecated, use secure_bytearray instead
SecureBuffer
) )
except ImportError: except ImportError:
pass pass
@ -36,7 +38,9 @@ __all__ = [
'get_memory_protection_info', 'get_memory_protection_info',
'disable_secure_memory', 'disable_secure_memory',
'enable_secure_memory', 'enable_secure_memory',
'secure_bytes' 'secure_bytearray',
'secure_bytes', # Deprecated, use secure_bytearray instead
'SecureBuffer'
] ]
__version__ = "0.1.0" __version__ = "0.1.0"