use core::fmt;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub type SecretWif = SecretAscii<52>;
pub type SecretKeyHex = SecretAscii<64>;
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SecretAscii<const N: usize> {
bytes: Box<[u8; N]>,
}
impl<const N: usize> SecretAscii<N> {
pub(crate) fn zeroed() -> Self {
Self {
bytes: Box::new([0u8; N]),
}
}
pub(crate) fn bytes_mut(&mut self) -> &mut [u8; N] {
&mut self.bytes
}
#[must_use]
pub fn expose_bytes(&self) -> &[u8; N] {
&self.bytes
}
#[must_use]
pub fn expose_str(&self) -> &str {
core::str::from_utf8(self.expose_bytes()).expect("SecretAscii holds ASCII")
}
}
impl<const N: usize> fmt::Debug for SecretAscii<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Secret<{N}>([REDACTED])")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug_redacts_contents() {
let mut secret = SecretWif::zeroed();
secret
.bytes_mut()
.copy_from_slice(b"KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn");
let debug = format!("{:?}", secret);
assert_eq!(debug, "Secret<52>([REDACTED])");
assert!(!debug.contains("KwDi"), "Debug must not leak the WIF");
}
#[test]
fn test_expose_str_round_trips() {
let mut secret = SecretKeyHex::zeroed();
let hex = b"0000000000000000000000000000000000000000000000000000000000000001";
secret.bytes_mut().copy_from_slice(hex);
assert_eq!(secret.expose_str(), std::str::from_utf8(hex).unwrap());
assert_eq!(secret.expose_bytes(), hex);
}
#[test]
fn test_zeroed_starts_empty() {
assert_eq!(SecretWif::zeroed().expose_bytes(), &[0u8; 52]);
}
}