use std::fmt;
use crate::credentials::Secret;
const REDACTED: &str = "SecretString(<redacted>)";
#[derive(Clone, PartialEq, Eq)]
pub struct SecretString(String);
impl SecretString {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn expose(&self) -> &str {
&self.0
}
pub fn into_secret(self) -> Secret<String> {
Secret::new(self.0)
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
impl fmt::Display for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_debug_is_redacted() {
let s = SecretString::new("sk-or-verysecretvalue1234"); let dumped = format!("{s:?}");
assert!(!dumped.contains("verysecretvalue"), "leaked: {dumped}");
assert!(!dumped.contains("sk-o"), "head leaked: {dumped}");
assert!(!dumped.contains("chars"), "length leaked: {dumped}");
assert_eq!(dumped, REDACTED);
}
#[test]
fn secret_display_is_redacted() {
let s = SecretString::new("sk-or-verysecretvalue1234"); let shown = format!("{s}");
assert!(!shown.contains("verysecretvalue"), "leaked: {shown}");
assert!(!shown.contains("sk-o"), "head leaked: {shown}");
assert_eq!(shown, REDACTED);
}
#[test]
fn secret_expose_returns_raw() {
let s = SecretString::new("raw-key"); assert_eq!(s.expose(), "raw-key");
}
fn specimens() -> Vec<String> {
let mut v: Vec<String> = [
"",
"a",
"secret",
"ghp_16C7e42F292c6912E7710c838347Ae178B4a",
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.dBjftJeZ4CVP",
"パスワード",
"SecretString(<redacted>)",
]
.iter()
.map(|s| s.to_string())
.collect();
let mut state: u64 = 0x2545_F491_4F6C_DD1D;
for len in 1..=64 {
let mut s = String::with_capacity(len);
for _ in 0..len {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let byte = ((state >> 33) % 94) as u8 + 33; s.push(byte as char);
}
v.push(s);
}
v
}
#[test]
fn debug_and_display_are_value_independent() {
let baseline_debug = format!("{:?}", SecretString::new(""));
let baseline_display = format!("{}", SecretString::new(""));
for value in specimens() {
let s = SecretString::new(value.clone());
assert_eq!(
format!("{s:?}"),
baseline_debug,
"Debug varied with the value: {value:?}"
);
assert_eq!(
format!("{s}"),
baseline_display,
"Display varied with the value: {value:?}"
);
}
}
#[test]
fn rendering_contains_no_substring_of_the_value() {
const MIN_DISCLOSURE_LEN: usize = 3;
const MIN_CREDENTIAL_LEN: usize = 8;
let empty = SecretString::new("");
let baseline = format!("{empty:?} {empty}");
for value in specimens() {
let chars: Vec<char> = value.chars().collect();
if chars.len() < MIN_CREDENTIAL_LEN {
continue;
}
let s = SecretString::new(value.clone());
let rendered = format!("{s:?} {s}");
for start in 0..chars.len() {
for end in (start + MIN_DISCLOSURE_LEN)..=chars.len() {
let candidate: String = chars[start..end].iter().collect();
if baseline.contains(&candidate) {
continue; }
assert!(
!rendered.contains(&candidate),
"rendering {rendered:?} disclosed {candidate:?} from {value:?}"
);
}
}
}
}
#[test]
fn secret_string_converts_to_the_canonical_secret() {
let s = SecretString::new("sk-or-verysecretvalue1234"); let before = format!("{s:?}");
assert!(!before.contains("sk-o"), "head leaked: {before}");
let tightened = s.into_secret();
assert_eq!(tightened.expose(), "sk-or-verysecretvalue1234");
let rendered = format!("{tightened:?} {tightened}");
assert!(!rendered.contains("sk-o"), "head survived: {rendered}");
assert!(!rendered.contains("verysecretvalue"), "leaked: {rendered}");
}
}