pub type ZeroBuf<const N: usize> = zeroize::Zeroizing<[u8; N]>;
macro_rules! secret_newtype {
(
$(#[$attr:meta])*
$name:ident($n:literal)
) => {
$(#[$attr])*
#[repr(transparent)]
#[derive(Clone, PartialEq, Eq, zeroize::Zeroize)]
pub struct $name(ZeroBuf<$n>);
impl $name {
pub fn new(bytes: ZeroBuf<$n>) -> Self {
Self(bytes)
}
#[allow(dead_code)]
pub fn as_bytes(&self) -> &[u8; $n] {
&*self.0
}
}
impl AsRef<[u8; $n]> for $name {
fn as_ref(&self) -> &[u8; $n] {
&*self.0
}
}
impl From<[u8; $n]> for $name {
fn from(bytes: [u8; $n]) -> Self {
Self(ZeroBuf::<$n>::new(bytes))
}
}
};
}
secret_newtype! {
Secret(32)
}
impl core::fmt::Debug for Secret {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("Secret([redacted; 32])")
}
}
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct TranscriptDigest(pub [u8; 32]);
impl TranscriptDigest {
pub const fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl AsRef<[u8; 32]> for TranscriptDigest {
fn as_ref(&self) -> &[u8; 32] {
&self.0
}
}
impl From<[u8; 32]> for TranscriptDigest {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl core::fmt::Debug for TranscriptDigest {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("TranscriptDigest").field(&self.0).finish()
}
}
secret_newtype! {
AeadIv(12)
}
impl core::fmt::Debug for AeadIv {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("AeadIv([redacted; 12])")
}
}
#[cfg(test)]
pub(crate) mod tests {
extern crate alloc;
use super::*;
use alloc::format;
use zeroize::Zeroize;
secret_newtype! {
AeadKey(16)
}
impl core::fmt::Debug for AeadKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("AeadKey([redacted; 16])")
}
}
#[cfg(feature = "cipher-aes")]
impl AeadKey {
pub(crate) fn as_zeroizing(&self) -> &ZeroBuf<16> {
&self.0
}
}
#[cfg(feature = "chacha20")]
secret_newtype! {
AeadKey32(32)
}
#[cfg(feature = "chacha20")]
impl core::fmt::Debug for AeadKey32 {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("AeadKey32([redacted; 32])")
}
}
#[cfg(feature = "chacha20")]
impl AeadKey32 {
pub(crate) fn as_zeroizing(&self) -> &ZeroBuf<32> {
&self.0
}
}
#[test]
fn newtype_round_trip() {
let s = Secret::new(ZeroBuf::<32>::new([0x42; 32]));
assert_eq!(s.as_bytes(), &[0x42; 32]);
let key = AeadKey::new(ZeroBuf::<16>::new([0x11; 16]));
assert_eq!(key.as_bytes(), &[0x11; 16]);
let iv = AeadIv::new(ZeroBuf::<12>::new([0x22; 12]));
assert_eq!(iv.as_bytes(), &[0x22; 12]);
let td = TranscriptDigest::new([0x33; 32]);
assert_eq!(td.as_bytes(), &[0x33; 32]);
}
#[test]
fn from_array_round_trip() {
let s = Secret::from([0x42; 32]);
assert_eq!(s.as_bytes(), &[0x42; 32]);
let key: AeadKey = [0x11; 16].into();
assert_eq!(key.as_bytes(), &[0x11; 16]);
let iv: AeadIv = [0x22; 12].into();
assert_eq!(iv.as_bytes(), &[0x22; 12]);
}
#[test]
fn secret_zeroizes() {
let mut s = Secret::from([0x42; 32]);
s.zeroize();
assert_eq!(s.as_bytes(), &[0u8; 32]);
}
#[test]
fn secret_debug_does_not_leak_bytes() {
let s = Secret::new(ZeroBuf::<32>::new([0xAB; 32]));
let dbg = format!("{s:?}");
assert!(!dbg.contains("171"), "Debug leaks decimal bytes: {dbg}");
assert!(!dbg.contains("0xab"), "Debug leaks hex bytes: {dbg}");
assert!(!dbg.contains("AB"), "Debug leaks hex bytes: {dbg}");
assert!(dbg.contains("redacted"));
}
#[test]
fn aead_key_debug_does_not_leak_bytes() {
let k = AeadKey::new(ZeroBuf::<16>::new([0xCD; 16]));
let dbg = format!("{k:?}");
assert!(!dbg.contains("205"), "Debug leaks decimal bytes: {dbg}");
assert!(!dbg.contains("CD"), "Debug leaks hex bytes: {dbg}");
assert!(dbg.contains("redacted"));
}
#[test]
fn aead_iv_debug_does_not_leak_bytes() {
let iv = AeadIv::new(ZeroBuf::<12>::new([0xEF; 12]));
let dbg = format!("{iv:?}");
assert!(!dbg.contains("239"), "Debug leaks decimal bytes: {dbg}");
assert!(!dbg.contains("EF"), "Debug leaks hex bytes: {dbg}");
assert!(dbg.contains("redacted"));
}
#[test]
fn transcript_digest_debug_prints_bytes() {
let td = TranscriptDigest::new([0x11; 32]);
let dbg = format!("{td:?}");
assert!(dbg.contains("17") || dbg.contains("0x11"));
}
#[test]
fn zero_buf_round_trip() {
let mut buf = ZeroBuf::<16>::new([0u8; 16]);
assert_eq!(*buf, [0u8; 16]);
buf.copy_from_slice(&[0x42; 16]);
assert_eq!(*buf, [0x42u8; 16]);
let arr: [u8; 16] = *buf;
assert_eq!(arr, [0x42u8; 16]);
}
}