Skip to main content

loonfs_api/
secret.rs

1//! A string wrapper that keeps credential material out of logs and debug
2//! output.
3
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7/// The placeholder printed in place of secret material.
8const REDACTED: &str = "<redacted>";
9
10/// A secret string such as an access key, token, or signing secret.
11///
12/// `Debug` and `Display` both print `<redacted>` so secrets never leak
13/// through logging, tracing, or error formatting. Call [`SecretString::expose`]
14/// at the sites that genuinely need the raw value (request signing, provider
15/// builders, config persistence).
16///
17/// Serde serialization is transparent and **writes the actual secret** —
18/// config files need the real value round-tripped — so never serialize a
19/// secret-bearing struct into logs or display output; use a redacted copy
20/// (see [`SecretString::masked`]) instead.
21#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct SecretString(String);
24
25impl SecretString {
26    /// Wraps a secret value.
27    pub fn new(value: impl Into<String>) -> Self {
28        Self(value.into())
29    }
30
31    /// Returns whether the secret contains only whitespace.
32    pub fn is_blank(&self) -> bool {
33        self.0.trim().is_empty()
34    }
35
36    /// Returns the raw secret. Keep the exposure site as small as possible.
37    pub fn expose(&self) -> &str {
38        &self.0
39    }
40
41    /// Returns a copy whose *stored value* is the redaction placeholder.
42    ///
43    /// Use this to build display-safe copies of config structs that are
44    /// subsequently serialized (serde serialization is transparent and would
45    /// otherwise write the real secret).
46    pub fn masked(&self) -> Self {
47        Self(REDACTED.to_owned())
48    }
49}
50
51impl fmt::Debug for SecretString {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str(REDACTED)
54    }
55}
56
57impl fmt::Display for SecretString {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.write_str(REDACTED)
60    }
61}
62
63impl From<String> for SecretString {
64    fn from(value: String) -> Self {
65        Self(value)
66    }
67}
68
69impl From<&str> for SecretString {
70    fn from(value: &str) -> Self {
71        Self(value.to_owned())
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::SecretString;
78
79    #[test]
80    fn debug_and_display_redact_the_value() {
81        let secret = SecretString::new("super-secret-value");
82
83        assert_eq!(format!("{secret:?}"), "<redacted>");
84        assert_eq!(format!("{secret}"), "<redacted>");
85        assert_eq!(secret.expose(), "super-secret-value");
86    }
87
88    #[test]
89    fn blank_detection_ignores_surrounding_whitespace() {
90        assert!(SecretString::new(" \t\n").is_blank());
91        assert!(!SecretString::new(" secret ").is_blank());
92    }
93
94    #[test]
95    fn serde_round_trips_the_raw_value() {
96        let secret = SecretString::new("super-secret-value");
97
98        let encoded = serde_json::to_string(&secret).expect("serialize secret");
99        assert_eq!(encoded, "\"super-secret-value\"");
100
101        let decoded: SecretString = serde_json::from_str(&encoded).expect("deserialize secret");
102        assert_eq!(decoded, secret);
103    }
104}