use core::fmt;
use zeroize::{Zeroize, ZeroizeOnDrop};
const REDACTED: &str = "REDACTED";
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretString(String);
impl SecretString {
#[inline]
#[must_use]
pub const fn new(value: String) -> Self {
Self(value)
}
#[inline]
#[must_use]
pub fn expose_secret(&self) -> &str {
&self.0
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
#[must_use]
pub fn into_string(mut self) -> String {
core::mem::take(&mut self.0)
}
}
impl From<String> for SecretString {
#[inline]
fn from(value: String) -> Self {
Self::new(value)
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecretString({REDACTED})")
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretBytes(Vec<u8>);
impl SecretBytes {
#[inline]
#[must_use]
pub const fn new(value: Vec<u8>) -> Self {
Self(value)
}
#[inline]
#[must_use]
pub fn expose_secret(&self) -> &[u8] {
&self.0
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
#[must_use]
pub fn into_vec(mut self) -> Vec<u8> {
core::mem::take(&mut self.0)
}
}
impl From<Vec<u8>> for SecretBytes {
#[inline]
fn from(value: Vec<u8>) -> Self {
Self::new(value)
}
}
impl fmt::Debug for SecretBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecretBytes({REDACTED})")
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretArray<const N: usize>([u8; N]);
impl<const N: usize> SecretArray<N> {
#[inline]
#[must_use]
pub const fn new(value: [u8; N]) -> Self {
Self(value)
}
#[inline]
#[must_use]
pub const fn expose_secret(&self) -> &[u8] {
&self.0
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
N
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
N == 0
}
#[inline]
#[must_use]
pub const fn into_inner(self) -> [u8; N] {
let inner = self.0;
core::mem::forget(self);
inner
}
}
impl<const N: usize> From<[u8; N]> for SecretArray<N> {
#[inline]
fn from(value: [u8; N]) -> Self {
Self::new(value)
}
}
impl<const N: usize> fmt::Debug for SecretArray<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecretArray({REDACTED})")
}
}
#[cfg(test)]
mod tests {
use super::{SecretArray, SecretBytes, SecretString};
const fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
const fn assert_traits() {
assert_zeroize_on_drop::<SecretString>();
assert_zeroize_on_drop::<SecretBytes>();
assert_zeroize_on_drop::<SecretArray<32>>();
}
const _: () = assert_traits();
#[test]
fn secret_string_round_trip() {
let s = SecretString::new("hunter2".to_string());
assert_eq!(s.expose_secret(), "hunter2");
assert_eq!(s.len(), 7);
assert!(!s.is_empty());
assert!(SecretString::from(String::new()).is_empty());
assert_eq!(s.into_string(), "hunter2");
}
#[test]
fn secret_string_from() {
let s: SecretString = "abc".to_string().into();
assert_eq!(s.expose_secret(), "abc");
}
#[test]
fn secret_string_debug_redacted() {
let secret = "super-secret-seed-value";
let s = SecretString::new(secret.to_string());
let dbg = format!("{s:?}");
assert_eq!(dbg, "SecretString(REDACTED)");
assert!(!dbg.contains(secret));
assert!(!dbg.contains("23"));
}
#[test]
fn secret_string_clone_is_independent_allocation() {
let original = SecretString::new("independent".to_string());
let cloned = original.clone();
assert_eq!(original.expose_secret(), cloned.expose_secret());
assert_ne!(
original.expose_secret().as_ptr(),
cloned.expose_secret().as_ptr(),
"clone must own a separate allocation"
);
drop(cloned);
assert_eq!(original.expose_secret(), "independent");
}
#[test]
fn secret_bytes_round_trip() {
let b = SecretBytes::new(vec![1, 2, 3, 4]);
assert_eq!(b.expose_secret(), &[1, 2, 3, 4]);
assert_eq!(b.len(), 4);
assert!(!b.is_empty());
assert!(SecretBytes::from(Vec::new()).is_empty());
assert_eq!(b.into_vec(), vec![1, 2, 3, 4]);
}
#[test]
fn secret_bytes_from() {
let b: SecretBytes = vec![9u8, 8, 7].into();
assert_eq!(b.expose_secret(), &[9, 8, 7]);
}
#[test]
fn secret_bytes_debug_redacted() {
let secret = [0xde, 0xad, 0xbe, 0xef];
let b = SecretBytes::new(secret.to_vec());
let dbg = format!("{b:?}");
assert_eq!(dbg, "SecretBytes(REDACTED)");
assert!(!dbg.contains("222") && !dbg.contains("173"));
assert!(!dbg.contains("de") && !dbg.contains("ad"));
assert!(!dbg.contains('4'));
}
#[test]
fn secret_bytes_clone_is_independent_allocation() {
let original = SecretBytes::new(vec![10, 20, 30]);
let cloned = original.clone();
assert_eq!(original.expose_secret(), cloned.expose_secret());
assert_ne!(
original.expose_secret().as_ptr(),
cloned.expose_secret().as_ptr(),
"clone must own a separate allocation"
);
drop(cloned);
assert_eq!(original.expose_secret(), &[10, 20, 30]);
}
#[test]
fn secret_array_round_trip() {
let key = [7u8; 32];
let a = SecretArray::<32>::new(key);
assert_eq!(a.expose_secret(), &key);
assert_eq!(a.len(), 32);
assert!(!a.is_empty());
assert_eq!(a.into_inner(), key);
}
#[test]
fn secret_array_from() {
let a: SecretArray<4> = [1u8, 2, 3, 4].into();
assert_eq!(a.expose_secret(), &[1, 2, 3, 4]);
}
#[test]
fn secret_array_empty() {
let a = SecretArray::<0>::new([]);
assert!(a.is_empty());
assert_eq!(a.len(), 0);
}
#[test]
fn secret_array_debug_redacted() {
let key = [0xABu8; 16];
let a = SecretArray::<16>::new(key);
let dbg = format!("{a:?}");
assert_eq!(dbg, "SecretArray(REDACTED)");
assert!(!dbg.contains("171")); assert!(!dbg.contains("ab"));
assert!(!dbg.contains("16")); }
#[test]
fn secret_array_clone_is_independent_value() {
let original = SecretArray::<32>::new([5u8; 32]);
let mut cloned = original.clone();
assert_eq!(original.expose_secret(), cloned.expose_secret());
cloned = SecretArray::<32>::new([6u8; 32]);
assert_eq!(original.expose_secret(), &[5u8; 32]);
assert_eq!(cloned.expose_secret(), &[6u8; 32]);
}
#[test]
fn secret_array_zeroize_overwrites_backing_bytes() {
use zeroize::Zeroize;
let mut a = SecretArray::<32>::new([0xFFu8; 32]);
assert_eq!(a.expose_secret(), &[0xFFu8; 32]);
a.zeroize();
assert_eq!(
a.expose_secret(),
&[0u8; 32],
"zeroize must overwrite the stack bytes"
);
}
#[test]
fn secret_string_zeroize_overwrites_backing_bytes() {
use zeroize::Zeroize;
let mut s = SecretString::new("secret".to_string());
s.zeroize();
assert!(s.is_empty());
}
#[test]
fn secret_bytes_zeroize_overwrites_backing_bytes() {
use zeroize::Zeroize;
let mut b = SecretBytes::new(vec![0xFFu8; 16]);
b.zeroize();
assert!(b.is_empty());
}
}