mmap-backed buffer locked against swapping (mlock/VirtualLock) and excluded from core dumps (MADV_DONTDUMP) where available, wiped on close. Not used for return values; intended as an 'into' target (and key input) to keep sensitive material out of the regular heap. Adapted from quantumpipe's securebuf module.
116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
"""SecureBuffer: locked, core-dump-excluded memory for sensitive data.
|
|
|
|
Backed by an anonymous mmap that is locked against swapping (mlock /
|
|
VirtualLock) and excluded from core dumps (MADV_DONTDUMP) where available.
|
|
The contents are wiped on close().
|
|
|
|
SecureBuffer is not used for return values; allocate one yourself and pass
|
|
it as an ``into`` target to keep plaintext/keys out of the regular heap:
|
|
|
|
with SecureBuffer(4096) as buf:
|
|
ciph.decrypt(key, nonce, ct, mac, into=buf)
|
|
|
|
Adapted from quantumpipe's securebuf module.
|
|
"""
|
|
|
|
import ctypes
|
|
import mmap
|
|
import os
|
|
from contextlib import suppress
|
|
|
|
from .util import wipe
|
|
|
|
__all__ = ["SecureBuffer"]
|
|
|
|
|
|
class SecureBuffer:
|
|
"""A fixed-size buffer in locked, non-dumped memory, wiped on close."""
|
|
|
|
def __init__(self, size: int):
|
|
if size <= 0:
|
|
raise ValueError(f"size must be positive, got {size}")
|
|
self._mmap = mmap.mmap(-1, size, access=mmap.ACCESS_WRITE)
|
|
try:
|
|
self._lock()
|
|
self._dontdump()
|
|
except BaseException:
|
|
self._mmap.close()
|
|
raise
|
|
|
|
@property
|
|
def closed(self) -> bool:
|
|
"""True if the buffer has been closed."""
|
|
return self._mmap.closed
|
|
|
|
def _addr(self) -> int:
|
|
return ctypes.addressof(ctypes.c_char.from_buffer(self._mmap))
|
|
|
|
def _lock(self) -> None:
|
|
addr, size = self._addr(), len(self._mmap)
|
|
if os.name == "nt":
|
|
api = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
api.VirtualLock.argtypes = (ctypes.c_void_p, ctypes.c_size_t)
|
|
api.VirtualLock.restype = ctypes.c_bool
|
|
if not api.VirtualLock(addr, size):
|
|
raise ctypes.WinError(ctypes.get_last_error())
|
|
else:
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
libc.mlock.argtypes = (ctypes.c_void_p, ctypes.c_size_t)
|
|
libc.mlock.restype = ctypes.c_int
|
|
if libc.mlock(addr, size):
|
|
err = ctypes.get_errno()
|
|
raise OSError(err, os.strerror(err))
|
|
|
|
def _unlock(self) -> None:
|
|
addr, size = self._addr(), len(self._mmap)
|
|
if os.name == "nt":
|
|
api = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
api.VirtualUnlock.argtypes = (ctypes.c_void_p, ctypes.c_size_t)
|
|
api.VirtualUnlock.restype = ctypes.c_bool
|
|
if not api.VirtualUnlock(addr, size):
|
|
raise ctypes.WinError(ctypes.get_last_error())
|
|
else:
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
libc.munlock.argtypes = (ctypes.c_void_p, ctypes.c_size_t)
|
|
libc.munlock.restype = ctypes.c_int
|
|
if libc.munlock(addr, size):
|
|
err = ctypes.get_errno()
|
|
raise OSError(err, os.strerror(err))
|
|
|
|
def _dontdump(self) -> None:
|
|
if hasattr(self._mmap, "madvise") and hasattr(mmap, "MADV_DONTDUMP"):
|
|
self._mmap.madvise(mmap.MADV_DONTDUMP)
|
|
|
|
def close(self) -> None:
|
|
"""Wipe the contents, unlock and release the memory. Idempotent."""
|
|
if self.closed:
|
|
return
|
|
wipe(self._mmap)
|
|
self._unlock()
|
|
self._mmap.close()
|
|
|
|
def __buffer__(self, flags: int) -> memoryview:
|
|
return memoryview(self._mmap)
|
|
|
|
def __getitem__(self, key):
|
|
return memoryview(self._mmap)[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
memoryview(self._mmap)[key] = value
|
|
|
|
def __bytes__(self):
|
|
raise TypeError("SecureBuffer cannot be converted to bytes")
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._mmap)
|
|
|
|
def __enter__(self) -> "SecureBuffer":
|
|
return self
|
|
|
|
def __exit__(self, *args) -> None:
|
|
self.close()
|
|
|
|
def __del__(self):
|
|
with suppress(Exception):
|
|
self.close()
|