Skip to main content

vgi_forge_github/
secret.rs

1//! Secret-bearing strings: zeroized on drop, never printed.
2
3use std::fmt;
4
5use zeroize::Zeroizing;
6
7/// A secret string — a token, a key, a webhook secret.
8///
9/// Wiped from memory when dropped, and its `Debug` is a fixed placeholder so
10/// a stray `{:?}` or `tracing::debug!(?x)` cannot write it to a log. There is
11/// deliberately no `Display`, `Serialize` or `Clone`: getting the value out
12/// takes an explicit [`Secret::expose`], which is easy to grep for.
13pub struct Secret(Zeroizing<String>);
14
15impl Secret {
16    /// Wrap a secret.
17    pub fn new(value: impl Into<String>) -> Self {
18        Secret(Zeroizing::new(value.into()))
19    }
20
21    /// The secret value. Keep the borrow short.
22    pub fn expose(&self) -> &str {
23        &self.0
24    }
25}
26
27impl fmt::Debug for Secret {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.write_str("Secret(<redacted>)")
30    }
31}
32
33impl From<String> for Secret {
34    fn from(value: String) -> Self {
35        Secret::new(value)
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn debug_never_shows_the_value() {
45        let s = Secret::new("ghs_supersecret");
46        assert_eq!(format!("{s:?}"), "Secret(<redacted>)");
47        assert_eq!(s.expose(), "ghs_supersecret");
48    }
49}