#[cfg(all(unix, not(target_arch = "wasm32")))]
use tracing::warn;
use zeroize::Zeroize;
pub struct SecureKey {
bytes: Box<[u8; 32]>,
}
impl SecureKey {
pub fn new(bytes: [u8; 32]) -> Self {
#[cfg_attr(not(all(unix, not(target_arch = "wasm32"))), allow(unused_mut))]
let mut boxed = Box::new(bytes);
#[cfg(all(unix, not(target_arch = "wasm32")))]
mlock_best_effort(
boxed.as_mut_ptr() as *mut libc::c_void,
std::mem::size_of_val(boxed.as_ref()),
);
Self { bytes: boxed }
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.bytes
}
}
impl std::fmt::Debug for SecureKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecureKey")
.field("bytes", &"[REDACTED]")
.finish()
}
}
impl Drop for SecureKey {
fn drop(&mut self) {
self.bytes.as_mut().zeroize();
#[cfg(all(unix, not(target_arch = "wasm32")))]
munlock_best_effort(
self.bytes.as_mut_ptr() as *mut libc::c_void,
std::mem::size_of_val(self.bytes.as_ref()),
);
}
}
pub fn mlock_key_bytes(bytes: &mut [u8]) {
#[cfg(all(unix, not(target_arch = "wasm32")))]
mlock_best_effort(bytes.as_mut_ptr() as *mut libc::c_void, bytes.len());
#[cfg(not(all(unix, not(target_arch = "wasm32"))))]
let _ = bytes;
}
#[cfg(all(unix, not(target_arch = "wasm32")))]
fn mlock_best_effort(ptr: *mut libc::c_void, len: usize) {
let rc = unsafe { libc::mlock(ptr, len) };
if rc != 0 {
warn!(
"mlock failed for {} bytes (errno {}): key may be swapped to disk \
under extreme memory pressure. Increase RLIMIT_MEMLOCK if this \
is a concern.",
len,
std::io::Error::last_os_error()
);
}
}
#[cfg(all(unix, not(target_arch = "wasm32")))]
fn munlock_best_effort(ptr: *mut libc::c_void, len: usize) {
unsafe {
libc::munlock(ptr, len);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secure_key_stores_bytes() {
let key = SecureKey::new([0x42u8; 32]);
assert_eq!(*key.as_bytes(), [0x42u8; 32]);
}
#[test]
fn secure_key_zeros_on_drop() {
let key = SecureKey::new([0xABu8; 32]);
drop(key);
}
#[test]
fn secure_key_debug_redacts_bytes() {
let key = SecureKey::new([0xCDu8; 32]);
let rendered = format!("{key:?}");
assert!(rendered.contains("[REDACTED]"));
assert!(!rendered.contains("205"));
assert!(!rendered.to_lowercase().contains("cd"));
}
#[test]
fn mlock_key_bytes_accepts_slice() {
let mut buf = [0u8; 32];
mlock_key_bytes(&mut buf);
}
#[test]
#[cfg(all(unix, not(target_arch = "wasm32")))]
fn mlock_graceful_on_linux() {
let mut buf = [0u8; 32];
mlock_best_effort(buf.as_mut_ptr() as *mut libc::c_void, 32);
munlock_best_effort(buf.as_mut_ptr() as *mut libc::c_void, 32);
}
}