use core::fmt;
use core::ops::{Deref, DerefMut};
use zeroize::Zeroize;
pub(crate) struct LockedSecret<const N: usize> {
buf: Box<[u8; N]>,
locked: bool,
}
impl<const N: usize> LockedSecret<N> {
pub(crate) fn zeroed() -> Self {
let mut buf = Box::new([0u8; N]);
let locked = lock(buf.as_mut_ptr(), N);
Self { buf, locked }
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn is_locked(&self) -> bool {
self.locked
}
}
impl<const N: usize> Deref for LockedSecret<N> {
type Target = [u8; N];
fn deref(&self) -> &[u8; N] {
&self.buf
}
}
impl<const N: usize> DerefMut for LockedSecret<N> {
fn deref_mut(&mut self) -> &mut [u8; N] {
&mut self.buf
}
}
impl<const N: usize> AsRef<[u8]> for LockedSecret<N> {
fn as_ref(&self) -> &[u8] {
&self.buf[..]
}
}
impl<const N: usize> AsMut<[u8]> for LockedSecret<N> {
fn as_mut(&mut self) -> &mut [u8] {
&mut self.buf[..]
}
}
impl<const N: usize> fmt::Debug for LockedSecret<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LockedSecret<{N}>([REDACTED], locked={})", self.locked)
}
}
impl<const N: usize> Drop for LockedSecret<N> {
fn drop(&mut self) {
self.buf.zeroize();
if self.locked {
unlock(self.buf.as_mut_ptr(), N);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unsafe_code)]
fn lock(ptr: *mut u8, len: usize) -> bool {
unsafe { memsec::mlock(ptr, len) }
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unsafe_code)]
fn unlock(ptr: *mut u8, len: usize) -> bool {
unsafe { memsec::munlock(ptr, len) }
}
#[cfg(target_arch = "wasm32")]
fn lock(_ptr: *mut u8, _len: usize) -> bool {
false
}
#[cfg(target_arch = "wasm32")]
fn unlock(_ptr: *mut u8, _len: usize) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zeroed_starts_all_zero() {
let s = LockedSecret::<32>::zeroed();
assert_eq!(*s, [0u8; 32]);
}
#[test]
fn deref_roundtrip() {
let mut s = LockedSecret::<32>::zeroed();
s.copy_from_slice(&[0xAB; 32]);
assert_eq!(s.as_ref(), &[0xAB; 32][..]);
assert_eq!(&s[..4], &[0xAB; 4][..]);
}
#[test]
fn debug_redacts_contents() {
let mut s = LockedSecret::<32>::zeroed();
s.copy_from_slice(&[0x42; 32]);
let dbg = format!("{s:?}");
assert!(dbg.contains("REDACTED"));
assert!(!dbg.contains("42"), "secret bytes leaked into Debug: {dbg}");
}
#[test]
fn lock_status_is_reported() {
let s = LockedSecret::<32>::zeroed();
let _ = s.is_locked();
}
}