use core::fmt;
use subtle::{Choice, ConstantTimeEq};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SecretBytes<const N: usize> {
bytes: [u8; N],
}
impl<const N: usize> SecretBytes<N> {
#[must_use]
#[inline]
pub const fn new(bytes: [u8; N]) -> Self {
Self { bytes }
}
#[must_use]
#[inline]
pub const fn zero() -> Self {
Self { bytes: [0u8; N] }
}
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
N
}
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
N == 0
}
#[must_use]
#[inline]
pub const fn expose_secret(&self) -> &[u8; N] {
&self.bytes
}
#[must_use]
#[inline]
pub fn clone_for_transmission(&self) -> Self {
Self { bytes: self.bytes }
}
}
impl<const N: usize> fmt::Debug for SecretBytes<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretBytes").field("len", &N).field("bytes", &"[REDACTED]").finish()
}
}
impl<const N: usize> ConstantTimeEq for SecretBytes<N> {
fn ct_eq(&self, other: &Self) -> Choice {
self.bytes.ct_eq(&other.bytes)
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SecretVec {
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
#[zeroize(skip)]
_lock: Option<MlockGuard>,
bytes: Vec<u8>,
}
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
struct MlockGuard(Option<region::LockGuard>);
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
impl Drop for MlockGuard {
fn drop(&mut self) {
let Some(guard) = self.0.take() else { return };
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
drop(guard);
}));
}
}
impl SecretVec {
#[must_use]
#[inline]
pub fn new(bytes: Vec<u8>) -> Self {
Self::from_bytes(bytes)
}
#[must_use]
#[inline]
pub fn zero(len: usize) -> Self {
Self::from_bytes(vec![0u8; len])
}
#[inline]
fn from_bytes(mut bytes: Vec<u8>) -> Self {
let len = bytes.len();
let mut owned: Vec<u8> = Vec::with_capacity(len);
owned.extend_from_slice(&bytes);
use zeroize::Zeroize;
bytes.zeroize();
drop(bytes);
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
let _lock = Self::try_lock(&owned);
Self {
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
_lock,
bytes: owned,
}
}
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
#[inline]
fn try_lock(bytes: &[u8]) -> Option<MlockGuard> {
if bytes.is_empty() {
return None;
}
region::lock(bytes.as_ptr(), bytes.len()).ok().map(|g| MlockGuard(Some(g)))
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
#[must_use]
#[inline]
pub fn expose_secret(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn clone_for_transmission(&self) -> Self {
Self::from_bytes(self.bytes.clone())
}
}
impl fmt::Debug for SecretVec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretVec")
.field("len", &self.bytes.len())
.field("bytes", &"[REDACTED]")
.finish()
}
}
impl ConstantTimeEq for SecretVec {
fn ct_eq(&self, other: &Self) -> Choice {
self.bytes.ct_eq(&other.bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_bytes_new_and_expose() {
let sb: SecretBytes<32> = SecretBytes::new([0x42u8; 32]);
assert_eq!(sb.expose_secret(), &[0x42u8; 32]);
assert_eq!(sb.len(), 32);
assert!(!sb.is_empty());
}
#[test]
fn secret_bytes_zero() {
let sb: SecretBytes<16> = SecretBytes::zero();
assert_eq!(sb.expose_secret(), &[0u8; 16]);
}
#[test]
fn secret_bytes_zero_sized() {
let sb: SecretBytes<0> = SecretBytes::new([]);
assert_eq!(sb.len(), 0);
assert!(sb.is_empty());
}
#[test]
fn secret_bytes_debug_is_redacted() {
let sb: SecretBytes<4> = SecretBytes::new([0xDE, 0xAD, 0xBE, 0xEF]);
let debug = format!("{:?}", sb);
assert!(debug.contains("[REDACTED]"));
assert!(debug.contains("len"));
assert!(!debug.contains("DE"));
assert!(!debug.contains("AD"));
assert!(!debug.contains("BE"));
assert!(!debug.contains("EF"));
assert!(!debug.contains("222")); }
#[test]
fn secret_bytes_ct_eq_equal() {
let a: SecretBytes<32> = SecretBytes::new([0x42u8; 32]);
let b: SecretBytes<32> = SecretBytes::new([0x42u8; 32]);
assert!(bool::from(a.ct_eq(&b)));
}
#[test]
fn secret_bytes_ct_eq_not_equal() {
let a: SecretBytes<32> = SecretBytes::new([0x42u8; 32]);
let mut different = [0x42u8; 32];
if let Some(first) = different.first_mut() {
*first = 0x41;
}
let b: SecretBytes<32> = SecretBytes::new(different);
assert!(!bool::from(a.ct_eq(&b)));
}
#[test]
fn secret_bytes_clone_for_transmission() {
let original: SecretBytes<16> = SecretBytes::new([0x55u8; 16]);
let copy = original.clone_for_transmission();
assert!(bool::from(original.ct_eq(©)));
drop(original);
assert_eq!(copy.expose_secret(), &[0x55u8; 16]);
}
#[test]
fn secret_bytes_large_n_compiles() {
let sb: SecretBytes<4896> = SecretBytes::zero();
assert_eq!(sb.len(), 4896);
}
#[test]
fn secret_vec_new_and_expose() {
let sv = SecretVec::new(vec![0x42u8; 48]);
assert_eq!(sv.expose_secret(), &[0x42u8; 48]);
assert_eq!(sv.len(), 48);
assert!(!sv.is_empty());
}
#[test]
fn secret_vec_zero() {
let sv = SecretVec::zero(24);
assert_eq!(sv.expose_secret(), &[0u8; 24]);
}
#[test]
fn secret_vec_empty() {
let sv = SecretVec::zero(0);
assert_eq!(sv.len(), 0);
assert!(sv.is_empty());
}
#[test]
fn secret_vec_debug_is_redacted() {
let sv = SecretVec::new(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let debug = format!("{:?}", sv);
assert!(debug.contains("[REDACTED]"));
assert!(debug.contains("len"));
assert!(!debug.contains("DE"));
assert!(!debug.contains("EF"));
}
#[test]
fn secret_vec_ct_eq_equal() {
let a = SecretVec::new(vec![0x42u8; 48]);
let b = SecretVec::new(vec![0x42u8; 48]);
assert!(bool::from(a.ct_eq(&b)));
}
#[test]
fn secret_vec_ct_eq_not_equal_same_len() {
let a = SecretVec::new(vec![0x42u8; 48]);
let mut different = vec![0x42u8; 48];
if let Some(last) = different.last_mut() {
*last = 0x41;
}
let b = SecretVec::new(different);
assert!(!bool::from(a.ct_eq(&b)));
}
#[test]
fn secret_vec_ct_eq_different_len() {
let a = SecretVec::new(vec![0x42u8; 32]);
let b = SecretVec::new(vec![0x42u8; 48]);
assert!(!bool::from(a.ct_eq(&b)));
}
#[test]
fn secret_vec_clone_for_transmission() {
let original = SecretVec::new(vec![0x77u8; 32]);
let copy = original.clone_for_transmission();
assert!(bool::from(original.ct_eq(©)));
drop(original);
assert_eq!(copy.expose_secret(), &[0x77u8; 32]);
}
#[test]
fn secret_bytes_manual_zeroize_clears() {
let mut sb: SecretBytes<16> = SecretBytes::new([0xAAu8; 16]);
sb.zeroize();
assert_eq!(sb.expose_secret(), &[0u8; 16]);
}
#[test]
fn secret_vec_manual_zeroize_clears() {
let mut sv = SecretVec::new(vec![0xAAu8; 16]);
sv.zeroize();
for &byte in sv.expose_secret() {
assert_eq!(byte, 0);
}
}
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
#[test]
fn secret_vec_mlock_new_succeeds() {
let sv = SecretVec::new(vec![0x42u8; 1024]);
assert_eq!(sv.len(), 1024);
assert_eq!(sv.expose_secret().first().copied(), Some(0x42));
}
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
#[test]
fn secret_vec_mlock_zero_succeeds() {
let sv = SecretVec::zero(256);
assert_eq!(sv.len(), 256);
assert!(sv.expose_secret().iter().all(|&b| b == 0));
}
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
#[test]
fn secret_vec_mlock_empty_does_not_lock() {
let sv = SecretVec::zero(0);
assert_eq!(sv.len(), 0);
assert!(sv.is_empty());
}
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
#[test]
fn secret_vec_mlock_clone_is_independent() {
let original = SecretVec::new(vec![0x77u8; 512]);
let copy = original.clone_for_transmission();
assert_eq!(copy.len(), 512);
assert_eq!(copy.expose_secret().first().copied(), Some(0x77));
drop(copy);
assert_eq!(original.expose_secret().first().copied(), Some(0x77));
}
#[cfg(all(feature = "secret-mlock", not(target_os = "windows")))]
#[test]
fn mlock_guard_drop_does_not_panic_under_repeated_lock_unlock() {
for _ in 0..32 {
let sv = SecretVec::new(vec![0xA5u8; 4096]);
assert_eq!(sv.expose_secret().len(), 4096);
drop(sv);
}
}
#[test]
fn secret_vec_from_bytes_handles_oversize_capacity_input() {
let mut over_alloc: Vec<u8> = Vec::with_capacity(1024);
over_alloc.extend_from_slice(&[0x5Au8; 64]);
assert_eq!(over_alloc.len(), 64);
assert!(over_alloc.capacity() >= 1024, "test setup: source must have slack capacity");
let sv = SecretVec::new(over_alloc);
assert_eq!(sv.len(), 64);
assert!(sv.expose_secret().iter().all(|&b| b == 0x5A));
assert_eq!(
sv.bytes.capacity(),
sv.bytes.len(),
"SecretVec must store the data with capacity == length so \
ZeroizeOnDrop covers every backing byte"
);
}
#[test]
fn secret_vec_zeroize_on_drop_clears_backing_storage() {
use zeroize::Zeroize;
let mut buf: Vec<u8> = vec![0xCDu8; 128];
buf.zeroize();
assert!(buf.iter().all(|&b| b == 0), "Vec::zeroize must clear all bytes in-place");
}
}