Skip to main content

synwire_core/credentials/
secret.rs

1//! Secret value type backed by the secrecy crate.
2
3use secrecy::{ExposeSecret, SecretString};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7/// A secret value with memory zeroisation on drop.
8///
9/// Debug and Display both render as `***`. Serialization produces `null`.
10#[derive(Clone)]
11pub struct SecretValue {
12    inner: SecretString,
13}
14
15impl SecretValue {
16    /// Creates a new secret value.
17    pub fn new(value: impl Into<String>) -> Self {
18        Self {
19            inner: SecretString::from(value.into()),
20        }
21    }
22
23    /// Exposes the secret value for explicit access.
24    pub fn expose(&self) -> &str {
25        self.inner.expose_secret()
26    }
27}
28
29impl fmt::Debug for SecretValue {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(f, "SecretValue(***)")
32    }
33}
34
35impl fmt::Display for SecretValue {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "***")
38    }
39}
40
41impl Serialize for SecretValue {
42    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43    where
44        S: serde::Serializer,
45    {
46        serializer.serialize_none()
47    }
48}
49
50impl<'de> Deserialize<'de> for SecretValue {
51    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
52    where
53        D: serde::Deserializer<'de>,
54    {
55        // SecretValue deserializes from null as an empty secret
56        Ok(Self::new(""))
57    }
58}
59
60impl PartialEq for SecretValue {
61    fn eq(&self, other: &Self) -> bool {
62        self.expose() == other.expose()
63    }
64}
65
66impl Eq for SecretValue {}
67
68impl std::hash::Hash for SecretValue {
69    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
70        self.expose().hash(state);
71    }
72}
73
74#[cfg(test)]
75#[allow(clippy::unwrap_used)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_secret_debug_redaction() {
81        let secret = SecretValue::new("my-api-key");
82        let debug = format!("{secret:?}");
83        assert!(!debug.contains("my-api-key"));
84        assert!(debug.contains("***"));
85    }
86
87    #[test]
88    fn test_secret_display_redaction() {
89        let secret = SecretValue::new("my-api-key");
90        let display = format!("{secret}");
91        assert_eq!(display, "***");
92    }
93
94    #[test]
95    fn test_secret_expose() {
96        let secret = SecretValue::new("my-api-key");
97        assert_eq!(secret.expose(), "my-api-key");
98    }
99
100    #[test]
101    fn test_secret_serialize_as_null() {
102        let secret = SecretValue::new("my-api-key");
103        let json = serde_json::to_string(&secret).unwrap();
104        assert_eq!(json, "null");
105    }
106
107    #[test]
108    fn test_secret_equality() {
109        let a = SecretValue::new("key");
110        let b = SecretValue::new("key");
111        let c = SecretValue::new("other");
112        assert_eq!(a, b);
113        assert_ne!(a, c);
114    }
115
116    #[test]
117    fn test_secret_clone() {
118        let secret = SecretValue::new("key");
119        let cloned = secret.clone();
120        assert_eq!(secret.expose(), cloned.expose());
121    }
122}