Skip to main content

nodedb_wal/
secure_mem.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Secure memory utilities for key material.
4//!
5//! Wraps `libc::mlock`/`munlock` to prevent key bytes from being swapped
6//! to disk. mlock is best-effort: if the OS refuses (e.g. RLIMIT_MEMLOCK
7//! exceeded on some container configurations), a warning is logged and
8//! startup continues. Failing to mlock does not expose the key — it only
9//! means the key could be paged out under extreme memory pressure.
10//!
11//! On platforms where mlock is not available (e.g. some WASM targets) the
12//! calls are no-ops.
13
14#[cfg(all(unix, not(target_arch = "wasm32")))]
15use tracing::warn;
16use zeroize::Zeroize;
17
18/// A 32-byte key held in memory, mlocked against swap.
19///
20/// On `Drop`, the memory is zeroed (via `zeroize`, which is guaranteed not to
21/// be elided by the optimizer) and then munlocked.
22///
23/// `Clone`, `Copy`, and `Debug` are intentionally NOT derived: key material
24/// must not be duplicated or written to logs. A redacting `Debug` impl is
25/// provided so the type can still appear in `#[derive(Debug)]` structs without
26/// leaking the bytes.
27pub struct SecureKey {
28    bytes: Box<[u8; 32]>,
29}
30
31impl SecureKey {
32    /// Wrap a 32-byte key, attempting to mlock it.
33    ///
34    /// If mlock fails, logs a warning and continues — startup is not aborted.
35    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    /// Access the key bytes.
47    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        // Zero the key before releasing. `zeroize` guarantees the writes are
63        // not optimized away and inserts a compiler fence afterwards.
64        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
73/// Public convenience wrapper for mlocking key bytes from `crypto.rs`.
74///
75/// Locks the whole slice. Best-effort: logs a warning on failure.
76/// No-op on non-Unix targets.
77pub 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/// Attempt to mlock `len` bytes starting at `ptr`.
85///
86/// Logs a warning if mlock fails.
87#[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/// Attempt to munlock `len` bytes starting at `ptr`. Best-effort, no error.
102#[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        // We can't observe zeroing from outside since the bytes move on drop,
122        // but this at least exercises the path without panic.
123        let key = SecureKey::new([0xABu8; 32]);
124        drop(key);
125        // If we get here without panic or memory error, mlock/munlock worked.
126    }
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        // The raw key byte (0xCD = 205) must not appear in any form.
134        assert!(!rendered.contains("205"));
135        assert!(!rendered.to_lowercase().contains("cd"));
136    }
137
138    #[test]
139    fn mlock_key_bytes_accepts_slice() {
140        // Best-effort mlock over a slice must not panic regardless of
141        // RLIMIT_MEMLOCK, and is a no-op on non-Unix targets.
142        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        // mlock with a stack pointer that may or may not succeed depending on
150        // RLIMIT_MEMLOCK. Either way we must not panic.
151        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        // Success = no panic.
155    }
156}