use std::fmt;
use secrecy::SecretBox;
pub use secrecy::ExposeSecret;
pub struct SecretString(secrecy::SecretString);
impl SecretString {
#[must_use]
pub fn new(secret: String) -> Self {
Self(secrecy::SecretString::from(secret))
}
#[must_use]
pub fn expose_secret(&self) -> &str {
ExposeSecret::expose_secret(&self.0)
}
#[must_use]
pub fn into_secret_bytes(self) -> SecretBytes {
SecretBytes::new(self.expose_secret().as_bytes().to_vec())
}
}
impl From<String> for SecretString {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for SecretString {
fn from(value: &str) -> Self {
Self::new(value.to_owned())
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SecretString([REDACTED])")
}
}
pub struct SecretBytes(SecretBox<Vec<u8>>);
impl SecretBytes {
#[must_use]
pub fn new(secret: Vec<u8>) -> Self {
Self(SecretBox::new(Box::new(secret)))
}
#[must_use]
pub fn empty() -> Self {
Self::new(Vec::new())
}
#[must_use]
pub fn expose_secret(&self) -> &[u8] {
ExposeSecret::expose_secret(&self.0).as_slice()
}
#[must_use]
pub fn len(&self) -> usize {
ExposeSecret::expose_secret(&self.0).len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
ExposeSecret::expose_secret(&self.0).is_empty()
}
}
impl fmt::Debug for SecretBytes {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SecretBytes([REDACTED])")
}
}
#[must_use]
pub fn secret_string_into_bytes(secret: SecretString) -> SecretBytes {
secret.into_secret_bytes()
}
pub trait SecretStringExt {
#[must_use]
fn into_secret_bytes(self) -> SecretBytes;
}
impl SecretStringExt for SecretString {
fn into_secret_bytes(self) -> SecretBytes {
SecretString::into_secret_bytes(self)
}
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
reason = "unit tests may assert with expect/unwrap"
)]
mod tests {
use super::{SecretBytes, SecretString, SecretStringExt};
use std::marker::PhantomData;
#[test]
fn debug_is_redacted() {
let string = SecretString::new(String::from("a unique string secret"));
let bytes = SecretBytes::new(b"unique byte secret".to_vec());
let string_debug = format!("{string:?}");
let bytes_debug = format!("{bytes:?}");
assert!(string_debug.contains("REDACTED"));
assert!(bytes_debug.contains("REDACTED"));
assert!(!string_debug.contains("unique"));
assert!(!bytes_debug.contains("unique"));
}
#[test]
fn secret_string_into_bytes_preserves_utf8() {
let string = SecretString::from("byte-roundtrip");
let bytes = SecretStringExt::into_secret_bytes(string);
assert_eq!(bytes.expose_secret(), b"byte-roundtrip");
}
#[test]
#[allow(
clippy::needless_borrow,
reason = "the explicit borrow is the probe: the Clone impl is reached at one autoref and the fallback at two, so removing it changes which trait answers"
)]
fn secret_string_is_not_clone() {
struct Probe<T>(PhantomData<T>);
trait NotClone {
fn is_clone(&self) -> bool {
false
}
}
impl<T> NotClone for &Probe<T> {}
trait IsClone {
fn is_clone(&self) -> bool {
true
}
}
impl<T: Clone> IsClone for Probe<T> {}
assert!(
(&Probe::<String>(PhantomData)).is_clone(),
"the probe must see a Clone type as Clone"
);
assert!(
!(&Probe::<SecretString>(PhantomData)).is_clone(),
"SecretString must not be Clone"
);
assert!(
!(&Probe::<SecretBytes>(PhantomData)).is_clone(),
"SecretBytes must not be Clone"
);
}
}