use zeroize::{Zeroize, ZeroizeOnDrop};
#[cfg(unix)]
fn lock_memory(ptr: *const u8, len: usize) -> bool {
if len == 0 {
return true;
}
unsafe { libc::mlock(ptr as *const libc::c_void, len) == 0 }
}
#[cfg(unix)]
fn unlock_memory(ptr: *const u8, len: usize) {
if len == 0 {
return;
}
unsafe {
libc::munlock(ptr as *const libc::c_void, len);
}
}
#[cfg(unix)]
fn exclude_from_core_dump(ptr: *const u8, len: usize) {
if len == 0 {
return;
}
#[cfg(target_os = "linux")]
unsafe {
libc::madvise(ptr as *mut libc::c_void, len, libc::MADV_DONTDUMP);
}
#[cfg(not(target_os = "linux"))]
let _ = (ptr, len);
}
#[cfg(not(unix))]
fn lock_memory(_ptr: *const u8, _len: usize) -> bool {
false
}
#[cfg(not(unix))]
fn unlock_memory(_ptr: *const u8, _len: usize) {}
#[cfg(not(unix))]
fn exclude_from_core_dump(_ptr: *const u8, _len: usize) {}
pub struct LockedVec {
inner: Vec<u8>,
locked: bool,
}
impl LockedVec {
pub fn new(data: Vec<u8>) -> Self {
let locked = lock_memory(data.as_ptr(), data.len());
if !locked && !data.is_empty() {
tracing::warn!(
bytes = data.len(),
"mlock failed for key material; memory may be swappable. \
This is non-fatal but reduces security on this platform."
);
}
exclude_from_core_dump(data.as_ptr(), data.len());
LockedVec {
inner: data,
locked,
}
}
pub fn is_locked(&self) -> bool {
self.locked
}
pub fn as_slice(&self) -> &[u8] {
&self.inner
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl AsRef<[u8]> for LockedVec {
fn as_ref(&self) -> &[u8] {
&self.inner
}
}
impl Zeroize for LockedVec {
fn zeroize(&mut self) {
self.inner.zeroize();
}
}
impl Drop for LockedVec {
fn drop(&mut self) {
self.inner.zeroize();
if self.locked {
unlock_memory(self.inner.as_ptr(), self.inner.capacity());
}
}
}
impl ZeroizeOnDrop for LockedVec {}
impl std::fmt::Debug for LockedVec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"LockedVec([REDACTED, {} bytes, locked={}])",
self.inner.len(),
self.locked
)
}
}
#[derive(Clone)]
pub struct ZeroizingVec(Vec<u8>);
impl ZeroizingVec {
pub fn new(data: Vec<u8>) -> Self {
ZeroizingVec(data)
}
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl AsRef<[u8]> for ZeroizingVec {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Zeroize for ZeroizingVec {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
impl Drop for ZeroizingVec {
fn drop(&mut self) {
self.zeroize();
}
}
impl ZeroizeOnDrop for ZeroizingVec {}
impl std::fmt::Debug for ZeroizingVec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ZeroizingVec([REDACTED, {} bytes])", self.0.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zeroizing_vec_basic() {
let data = vec![1, 2, 3, 4, 5];
let zv = ZeroizingVec::new(data);
assert_eq!(zv.as_slice(), &[1, 2, 3, 4, 5]);
assert_eq!(zv.len(), 5);
assert!(!zv.is_empty());
}
#[test]
fn test_zeroizing_vec_debug_redacted() {
let zv = ZeroizingVec::new(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let debug_str = format!("{:?}", zv);
assert!(debug_str.contains("REDACTED"));
assert!(!debug_str.contains("DE"));
assert!(!debug_str.contains("AD"));
}
#[test]
fn test_as_ref() {
let zv = ZeroizingVec::new(vec![1, 2, 3]);
let slice: &[u8] = zv.as_ref();
assert_eq!(slice, &[1, 2, 3]);
}
#[test]
fn test_locked_vec_basic_operations() {
let data = vec![10, 20, 30, 40, 50];
let lv = LockedVec::new(data);
assert_eq!(lv.as_slice(), &[10, 20, 30, 40, 50]);
assert_eq!(lv.len(), 5);
assert!(!lv.is_empty());
let slice: &[u8] = lv.as_ref();
assert_eq!(slice, &[10, 20, 30, 40, 50]);
}
#[test]
fn test_locked_vec_empty() {
let lv = LockedVec::new(vec![]);
assert!(lv.is_empty());
assert_eq!(lv.len(), 0);
let empty: &[u8] = &[];
assert_eq!(lv.as_slice(), empty);
assert!(lv.is_locked());
}
#[test]
fn test_locked_vec_zeroizes_on_drop() {
let mut lv = LockedVec::new(vec![0xAA_u8; 64]);
assert_eq!(lv.as_slice()[0], 0xAA);
lv.zeroize();
assert!(
lv.inner.is_empty(),
"inner Vec should be empty after zeroize"
);
let cap = lv.inner.capacity();
if cap > 0 {
let zeroed_bytes = unsafe { std::slice::from_raw_parts(lv.inner.as_ptr(), cap) };
let all_zero = zeroed_bytes.iter().all(|&b| b == 0);
assert!(
all_zero,
"LockedVec backing memory should be zeroed after zeroize (capacity={})",
cap
);
}
}
#[test]
fn test_locked_vec_debug_redacted() {
let lv = LockedVec::new(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let debug_str = format!("{:?}", lv);
assert!(
debug_str.contains("REDACTED"),
"Debug output should contain REDACTED, got: {}",
debug_str
);
assert!(
!debug_str.contains("222"), "Debug output should not leak byte values, got: {}",
debug_str
);
assert!(
debug_str.contains("locked="),
"Debug output should show lock status, got: {}",
debug_str
);
}
#[test]
fn test_locked_vec_mlock_called() {
let data = vec![1_u8; 128];
let lv = LockedVec::new(data);
if cfg!(unix) {
assert!(lv.is_locked(), "mlock should succeed for 128 bytes on Unix");
}
assert_eq!(lv.len(), 128);
}
#[test]
fn test_locked_vec_fallback_on_mlock_failure() {
let data = vec![42_u8; 256];
let lv = LockedVec::new(data);
assert_eq!(lv.as_slice()[0], 42);
assert_eq!(lv.len(), 256);
drop(lv);
}
#[test]
fn test_locked_vec_large_allocation() {
let data = vec![0xFF_u8; 8192];
let lv = LockedVec::new(data);
assert_eq!(lv.len(), 8192);
if cfg!(unix) {
assert!(
lv.is_locked(),
"mlock should succeed for 8192 bytes on Unix"
);
}
}
}