Skip to main content

loonfs_objectstore/
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 the raw secret. Keep the exposure site as small as possible.
32    pub fn expose(&self) -> &str {
33        &self.0
34    }
35
36    /// Returns a copy whose *stored value* is the redaction placeholder.
37    ///
38    /// Use this to build display-safe copies of config structs that are
39    /// subsequently serialized (serde serialization is transparent and would
40    /// otherwise write the real secret).
41    pub fn masked(&self) -> Self {
42        Self(REDACTED.to_owned())
43    }
44}
45
46impl fmt::Debug for SecretString {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(REDACTED)
49    }
50}
51
52impl fmt::Display for SecretString {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.write_str(REDACTED)
55    }
56}
57
58impl From<String> for SecretString {
59    fn from(value: String) -> Self {
60        Self(value)
61    }
62}
63
64impl From<&str> for SecretString {
65    fn from(value: &str) -> Self {
66        Self(value.to_owned())
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::SecretString;
73
74    #[test]
75    fn debug_and_display_redact_the_value() {
76        let secret = SecretString::new("super-secret-value");
77
78        assert_eq!(format!("{secret:?}"), "<redacted>");
79        assert_eq!(format!("{secret}"), "<redacted>");
80        assert_eq!(secret.expose(), "super-secret-value");
81    }
82
83    #[test]
84    fn masked_replaces_the_stored_value() {
85        let secret = SecretString::new("super-secret-value");
86
87        let masked = secret.masked();
88
89        assert_eq!(masked.expose(), "<redacted>");
90    }
91
92    #[test]
93    fn serde_round_trips_the_raw_value() {
94        let secret = SecretString::new("super-secret-value");
95
96        let encoded = serde_json::to_string(&secret).expect("serialize secret");
97        assert_eq!(encoded, "\"super-secret-value\"");
98
99        let decoded: SecretString = serde_json::from_str(&encoded).expect("deserialize secret");
100        assert_eq!(decoded, secret);
101    }
102}