Skip to main content

aurum_core/
secret.rs

1//! Redacting secret wrapper (JOE-1779 / JOE-1654).
2//!
3//! Secrets must never appear via Debug, Display, or accidental logging.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Opaque secret string. Debug/Display always print `***`.
9#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct SecretString(String);
12
13impl SecretString {
14    pub fn new(s: impl Into<String>) -> Self {
15        Self(s.into())
16    }
17
18    /// Access the raw secret. Prefer not logging the return value.
19    pub fn expose(&self) -> &str {
20        &self.0
21    }
22
23    pub fn into_inner(self) -> String {
24        self.0
25    }
26
27    pub fn is_empty(&self) -> bool {
28        self.0.is_empty()
29    }
30}
31
32impl fmt::Debug for SecretString {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.write_str("SecretString(***)")
35    }
36}
37
38impl fmt::Display for SecretString {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.write_str("***")
41    }
42}
43
44impl From<String> for SecretString {
45    fn from(s: String) -> Self {
46        Self(s)
47    }
48}
49
50impl From<&str> for SecretString {
51    fn from(s: &str) -> Self {
52        Self(s.to_string())
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn secret_never_in_debug_or_display() {
62        let s = SecretString::new("sk-super-secret");
63        let d = format!("{s:?}");
64        let disp = format!("{s}");
65        assert!(!d.contains("sk-super-secret"));
66        assert!(!disp.contains("sk-super-secret"));
67        assert!(d.contains("***"));
68        assert_eq!(s.expose(), "sk-super-secret");
69    }
70}