Coverage for src / kdbxtool / security / memory.py: 100%
45 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-19 21:22 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-19 21:22 +0000
1"""Secure memory handling for sensitive data.
3This module provides SecureBytes, a mutable byte container that:
4- Stores data in a bytearray (mutable, can be zeroized)
5- Automatically zeroizes memory on destruction
6- Supports context manager protocol for guaranteed cleanup
7- Prevents accidental exposure through repr/str
8"""
10from __future__ import annotations
12from typing import Self
15class SecureBytes:
16 """A secure container for sensitive byte data that zeroizes on destruction.
18 Unlike Python's immutable `bytes`, SecureBytes uses a mutable `bytearray`
19 that can be explicitly zeroed when no longer needed. This prevents sensitive
20 data like passwords and cryptographic keys from lingering in memory.
22 Usage:
23 # Basic usage
24 key = SecureBytes(derived_key_bytes)
25 # ... use key.data for crypto operations ...
26 key.zeroize() # Explicit cleanup
28 # Context manager (recommended)
29 with SecureBytes(password.encode()) as pwd:
30 hash = sha256(pwd.data)
31 # Automatically zeroized here
33 Note:
34 While this provides defense-in-depth against memory disclosure attacks,
35 Python's memory management means copies may still exist. For maximum
36 security, consider using specialized libraries like PyNaCl's SecretBox.
37 """
39 __slots__ = ("_buffer", "_zeroized")
41 def __init__(self, data: bytes | bytearray) -> None:
42 """Initialize with sensitive data.
44 Args:
45 data: The sensitive bytes to protect. Will be copied into internal buffer.
46 """
47 self._buffer = bytearray(data)
48 self._zeroized = False
50 @property
51 def data(self) -> bytes:
52 """Access the underlying data as immutable bytes.
54 Returns:
55 The protected data as bytes.
57 Raises:
58 ValueError: If the buffer has already been zeroized.
59 """
60 if self._zeroized:
61 raise ValueError("SecureBytes has been zeroized")
62 return bytes(self._buffer)
64 def __len__(self) -> int:
65 """Return the length of the protected data."""
66 return len(self._buffer)
68 def __bool__(self) -> bool:
69 """Return True if buffer contains data and hasn't been zeroized."""
70 return not self._zeroized and len(self._buffer) > 0
72 def zeroize(self) -> None:
73 """Overwrite the buffer with zeros.
75 This method overwrites every byte in the buffer with 0x00,
76 making the original data unrecoverable from this object.
77 Safe to call multiple times.
78 """
79 if not self._zeroized:
80 for i in range(len(self._buffer)):
81 self._buffer[i] = 0
82 self._zeroized = True
84 def __del__(self) -> None:
85 """Ensure buffer is zeroized when object is garbage collected."""
86 self.zeroize()
88 def __enter__(self) -> Self:
89 """Context manager entry."""
90 return self
92 def __exit__(
93 self,
94 exc_type: type[BaseException] | None,
95 exc_val: BaseException | None,
96 exc_tb: object,
97 ) -> None:
98 """Context manager exit - always zeroize."""
99 self.zeroize()
101 def __repr__(self) -> str:
102 """Safe repr that doesn't expose data."""
103 if self._zeroized:
104 return "SecureBytes(<zeroized>)"
105 return f"SecureBytes(<{len(self._buffer)} bytes>)"
107 def __str__(self) -> str:
108 """Safe str that doesn't expose data."""
109 return self.__repr__()
111 def __eq__(self, other: object) -> bool:
112 """Constant-time comparison to prevent timing attacks.
114 Uses hmac.compare_digest for constant-time comparison.
115 """
116 if not isinstance(other, SecureBytes):
117 return NotImplemented
118 if self._zeroized or other._zeroized:
119 return False
121 import hmac
123 return hmac.compare_digest(self._buffer, other._buffer)
125 def __hash__(self) -> int:
126 """Raise TypeError - SecureBytes should not be hashable."""
127 raise TypeError("SecureBytes is not hashable")
129 @classmethod
130 def from_str(cls, s: str, encoding: str = "utf-8") -> Self:
131 """Create SecureBytes from a string.
133 Args:
134 s: String to encode
135 encoding: Character encoding (default: utf-8)
137 Returns:
138 SecureBytes containing the encoded string
139 """
140 return cls(s.encode(encoding))