#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#[cfg(any(feature = "alloc", feature = "std"))]
extern crate alloc;
use core::fmt;
#[cfg(any(feature = "alloc", feature = "std"))]
use alloc::string::String;
const REDACTED: &str = "[REDACTED]";
pub trait ExposeSecret<T: ?Sized> {
fn expose_secret(&self) -> &T;
}
pub trait ExposeSecretMut<T: ?Sized>: ExposeSecret<T> {
fn expose_secret_mut(&mut self) -> &mut T;
}
pub struct Secret<T> {
inner: T,
}
impl<T> Secret<T> {
pub const fn new(inner: T) -> Self {
Self { inner }
}
pub fn into_inner(self) -> T {
self.inner
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Secret<U> {
Secret::new(f(self.inner))
}
}
impl<T: AsRef<[u8]>> Secret<T> {
pub fn ct_eq(&self, other: impl AsRef<[u8]>) -> bool {
let a = self.inner.as_ref();
let b = other.as_ref();
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
core::hint::black_box(diff) == 0
}
}
impl<T> ExposeSecret<T> for Secret<T> {
fn expose_secret(&self) -> &T {
&self.inner
}
}
impl<T> ExposeSecretMut<T> for Secret<T> {
fn expose_secret_mut(&mut self) -> &mut T {
&mut self.inner
}
}
impl<T: Clone> Clone for Secret<T> {
fn clone(&self) -> Self {
Self::new(self.inner.clone())
}
}
impl<T: Default> Default for Secret<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Secret(")?;
f.write_str(REDACTED)?;
f.write_str(")")
}
}
impl<T> fmt::Display for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
impl<T> From<T> for Secret<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub type SecretString = Secret<String>;
#[cfg(any(feature = "alloc", feature = "std"))]
impl SecretString {
pub fn from_string(value: impl Into<String>) -> Self {
Self::new(value.into())
}
pub fn expose_str(&self) -> &str {
self.inner.as_str()
}
}
#[cfg(test)]
mod tests {
use super::{ExposeSecret, ExposeSecretMut, Secret, SecretString};
use alloc::format;
use alloc::string::ToString;
#[test]
fn debug_redacts_secret() {
let secret = Secret::new("token-123");
assert_eq!(format!("{secret:?}"), "Secret([REDACTED])");
}
#[test]
fn display_redacts_secret() {
let secret = Secret::new("token-123");
assert_eq!(secret.to_string(), "[REDACTED]");
}
#[test]
fn expose_secret_returns_inner_reference() {
let secret = Secret::new("token-123");
assert_eq!(secret.expose_secret(), &"token-123");
}
#[test]
fn expose_secret_mut_allows_explicit_mutation() {
let mut secret = Secret::new(1_u8);
*secret.expose_secret_mut() = 2;
assert_eq!(secret.expose_secret(), &2);
}
#[test]
fn into_inner_returns_inner_value() {
let secret = Secret::new("token-123");
assert_eq!(secret.into_inner(), "token-123");
}
#[test]
fn map_returns_new_secret() {
let secret = Secret::new("token").map(|value| value.len());
assert_eq!(secret.expose_secret(), &5);
assert_eq!(format!("{secret:?}"), "Secret([REDACTED])");
}
#[test]
fn clone_clones_inner_value_without_leaking_debug() {
let secret = Secret::new("token".to_string());
let cloned = secret.clone();
assert_eq!(cloned.expose_secret(), "token");
assert_eq!(format!("{cloned:?}"), "Secret([REDACTED])");
}
#[test]
fn secret_string_wraps_owned_string() {
let secret = SecretString::from_string("password");
assert_eq!(secret.expose_secret(), "password");
assert_eq!(secret.expose_str(), "password");
assert_eq!(secret.to_string(), "[REDACTED]");
}
#[test]
fn ct_eq_matches_equal_bytes() {
let secret = SecretString::from_string("s3cr3t-token");
assert!(secret.ct_eq("s3cr3t-token"));
assert!(secret.ct_eq(b"s3cr3t-token"));
}
#[test]
fn ct_eq_rejects_different_content_same_length() {
let secret = SecretString::from_string("s3cr3t-token");
assert!(!secret.ct_eq("s3cr3t-tokeX"));
assert!(!secret.ct_eq("X3cr3t-tokeX"));
}
#[test]
fn ct_eq_rejects_different_length() {
let secret = SecretString::from_string("abc");
assert!(!secret.ct_eq("ab"));
assert!(!secret.ct_eq("abcd"));
assert!(!secret.ct_eq(""));
}
#[test]
fn ct_eq_works_for_byte_arrays_without_alloc_types() {
let secret = Secret::new(*b"key-bytes");
assert!(secret.ct_eq(b"key-bytes"));
assert!(!secret.ct_eq(b"key-byteX"));
}
#[test]
fn ct_eq_empty_secret_matches_empty() {
let secret = SecretString::from_string("");
assert!(secret.ct_eq(""));
assert!(!secret.ct_eq("x"));
}
#[test]
fn ct_eq_between_two_secrets_via_expose() {
let a = SecretString::from_string("same-value");
let b = SecretString::from_string("same-value");
assert!(a.ct_eq(b.expose_secret()));
}
}