1#[cfg(all(unix, not(target_arch = "wasm32")))]
15use tracing::warn;
16use zeroize::Zeroize;
17
18pub struct SecureKey {
28 bytes: Box<[u8; 32]>,
29}
30
31impl SecureKey {
32 pub fn new(bytes: [u8; 32]) -> Self {
36 #[cfg_attr(not(all(unix, not(target_arch = "wasm32"))), allow(unused_mut))]
37 let mut boxed = Box::new(bytes);
38 #[cfg(all(unix, not(target_arch = "wasm32")))]
39 mlock_best_effort(
40 boxed.as_mut_ptr() as *mut libc::c_void,
41 std::mem::size_of_val(boxed.as_ref()),
42 );
43 Self { bytes: boxed }
44 }
45
46 pub fn as_bytes(&self) -> &[u8; 32] {
48 &self.bytes
49 }
50}
51
52impl std::fmt::Debug for SecureKey {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("SecureKey")
55 .field("bytes", &"[REDACTED]")
56 .finish()
57 }
58}
59
60impl Drop for SecureKey {
61 fn drop(&mut self) {
62 self.bytes.as_mut().zeroize();
65 #[cfg(all(unix, not(target_arch = "wasm32")))]
66 munlock_best_effort(
67 self.bytes.as_mut_ptr() as *mut libc::c_void,
68 std::mem::size_of_val(self.bytes.as_ref()),
69 );
70 }
71}
72
73pub fn mlock_key_bytes(bytes: &mut [u8]) {
78 #[cfg(all(unix, not(target_arch = "wasm32")))]
79 mlock_best_effort(bytes.as_mut_ptr() as *mut libc::c_void, bytes.len());
80 #[cfg(not(all(unix, not(target_arch = "wasm32"))))]
81 let _ = bytes;
82}
83
84#[cfg(all(unix, not(target_arch = "wasm32")))]
88fn mlock_best_effort(ptr: *mut libc::c_void, len: usize) {
89 let rc = unsafe { libc::mlock(ptr, len) };
90 if rc != 0 {
91 warn!(
92 "mlock failed for {} bytes (errno {}): key may be swapped to disk \
93 under extreme memory pressure. Increase RLIMIT_MEMLOCK if this \
94 is a concern.",
95 len,
96 std::io::Error::last_os_error()
97 );
98 }
99}
100
101#[cfg(all(unix, not(target_arch = "wasm32")))]
103fn munlock_best_effort(ptr: *mut libc::c_void, len: usize) {
104 unsafe {
105 libc::munlock(ptr, len);
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn secure_key_stores_bytes() {
115 let key = SecureKey::new([0x42u8; 32]);
116 assert_eq!(*key.as_bytes(), [0x42u8; 32]);
117 }
118
119 #[test]
120 fn secure_key_zeros_on_drop() {
121 let key = SecureKey::new([0xABu8; 32]);
124 drop(key);
125 }
127
128 #[test]
129 fn secure_key_debug_redacts_bytes() {
130 let key = SecureKey::new([0xCDu8; 32]);
131 let rendered = format!("{key:?}");
132 assert!(rendered.contains("[REDACTED]"));
133 assert!(!rendered.contains("205"));
135 assert!(!rendered.to_lowercase().contains("cd"));
136 }
137
138 #[test]
139 fn mlock_key_bytes_accepts_slice() {
140 let mut buf = [0u8; 32];
143 mlock_key_bytes(&mut buf);
144 }
145
146 #[test]
147 #[cfg(all(unix, not(target_arch = "wasm32")))]
148 fn mlock_graceful_on_linux() {
149 let mut buf = [0u8; 32];
152 mlock_best_effort(buf.as_mut_ptr() as *mut libc::c_void, 32);
153 munlock_best_effort(buf.as_mut_ptr() as *mut libc::c_void, 32);
154 }
156}