Skip to main content

secrets_rs/
secret.rs

1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
4
5use crate::{
6    error::{BindError, UnboundError, UrnParseError},
7    source::SourceRegistry,
8    urn::Urn,
9};
10
11/// Implemented by types that can be stored inside a [`Secret`].
12///
13/// Provides the conversion from raw bytes returned by a source, the type label
14/// used in masked values, and a measurement of the value's size.
15pub trait SecretValue: Sized {
16    /// Short label included in the masked value, e.g. `"string"`, `"bytes"`, `"json"`.
17    fn type_name() -> &'static str;
18
19    /// Construct a value from the raw bytes returned by a source.
20    fn from_bytes(bytes: Vec<u8>, urn: &str) -> Result<Self, BindError>;
21
22    /// Human-readable size/length included in the masked value, e.g. `"12"`.
23    fn masked_size(&self) -> String;
24}
25
26impl SecretValue for String {
27    fn type_name() -> &'static str {
28        "string"
29    }
30
31    fn from_bytes(bytes: Vec<u8>, urn: &str) -> Result<Self, BindError> {
32        String::from_utf8(bytes).map_err(|e| BindError::TypeConversion {
33            urn: urn.to_owned(),
34            detail: e.to_string(),
35        })
36    }
37
38    fn masked_size(&self) -> String {
39        self.chars().count().to_string()
40    }
41}
42
43impl SecretValue for Vec<u8> {
44    fn type_name() -> &'static str {
45        "bytes"
46    }
47
48    fn from_bytes(bytes: Vec<u8>, _urn: &str) -> Result<Self, BindError> {
49        Ok(bytes)
50    }
51
52    fn masked_size(&self) -> String {
53        self.len().to_string()
54    }
55}
56
57impl SecretValue for serde_json::Value {
58    fn type_name() -> &'static str {
59        "json"
60    }
61
62    fn from_bytes(bytes: Vec<u8>, urn: &str) -> Result<Self, BindError> {
63        serde_json::from_slice(&bytes).map_err(|e| BindError::TypeConversion {
64            urn: urn.to_owned(),
65            detail: e.to_string(),
66        })
67    }
68
69    fn masked_size(&self) -> String {
70        // Use the compact serialized length as the size metric.
71        self.to_string().len().to_string()
72    }
73}
74
75/// A secret value identified by a URN.
76///
77/// Before [`bind`](Secret::bind) is called the secret is *unbound*; accessing
78/// its value returns an [`UnboundError`]. All default display paths (
79/// [`Display`](fmt::Display), [`Debug`], serde serialization) emit the masked
80/// value, which is safe to include in logs.
81///
82/// # Masked value format
83///
84/// - Unbound: `urn:secrets-rs:env:KEY [UNBOUND]`
85/// - Bound:   `urn:secrets-rs:env:KEY [string:12]`
86pub struct Secret<T: SecretValue> {
87    urn: Urn,
88    value: Option<T>,
89}
90
91impl<T: SecretValue> Secret<T> {
92    /// Creates an unbound secret from a URN string.
93    pub fn new(urn_str: &str) -> Result<Self, UrnParseError> {
94        Ok(Self {
95            urn: urn_str.parse()?,
96            value: None,
97        })
98    }
99
100    /// Returns the URN that identifies this secret.
101    ///
102    /// The URN is available regardless of whether the secret has been bound.
103    /// It contains only the source identifier and secret name — never the
104    /// secret value — so it is safe to log, store, or compare.
105    ///
106    /// The primary use case is constructing a second unbound secret with the
107    /// same identity, for example to hand the same logical secret to two
108    /// independent subsystems that each bind it separately:
109    ///
110    /// ```rust
111    /// # use secrets_rs::Secret;
112    /// let original: Secret<String> =
113    ///     Secret::new("urn:secrets-rs:env:API_KEY").unwrap();
114    ///
115    /// // Create a second unbound secret with the same URN.
116    /// let copy: Secret<String> =
117    ///     Secret::new(&original.urn().to_string()).unwrap();
118    /// ```
119    pub fn urn(&self) -> &Urn {
120        &self.urn
121    }
122
123    /// Returns the secret value, or an [`UnboundError`] if not yet bound.
124    pub fn value(&self) -> Result<&T, UnboundError> {
125        self.value.as_ref().ok_or_else(|| UnboundError {
126            urn: self.urn.to_string(),
127        })
128    }
129
130    /// Returns the masked value string — safe to log or serialize by default.
131    pub fn masked_value(&self) -> String {
132        match &self.value {
133            None => format!("{} [UNBOUND]", self.urn),
134            Some(v) => format!("{} [{}:{}]", self.urn, T::type_name(), v.masked_size()),
135        }
136    }
137
138    /// Fetches the secret from the appropriate source in `registry` and stores
139    /// it. Returns [`BindError`] if the source is not registered or the lookup
140    /// fails.
141    pub fn bind(&mut self, registry: &SourceRegistry) -> Result<(), BindError> {
142        let urn_str = self.urn.to_string();
143        let source =
144            registry
145                .get(&self.urn.source_id)
146                .ok_or_else(|| BindError::SourceNotFound {
147                    source_id: self.urn.source_id.clone(),
148                })?;
149
150        let bytes = source.get(&self.urn.name).map_err(|e| {
151            use crate::error::SourceError;
152            match e {
153                SourceError::NotFound { name } => BindError::NameNotFound {
154                    source_id: self.urn.source_id.clone(),
155                    name,
156                },
157                other => BindError::Source {
158                    urn: urn_str.clone(),
159                    source: other,
160                },
161            }
162        })?;
163
164        self.value = Some(T::from_bytes(bytes, &urn_str)?);
165        Ok(())
166    }
167}
168
169impl<T: SecretValue> fmt::Display for Secret<T> {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.write_str(&self.masked_value())
172    }
173}
174
175/// Always displays the masked value — the real value is never revealed via Debug.
176impl<T: SecretValue> fmt::Debug for Secret<T> {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "Secret({})", self.masked_value())
179    }
180}
181
182impl<T: SecretValue> Serialize for Secret<T> {
183    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
184        serializer.serialize_str(&self.masked_value())
185    }
186}
187
188/// Deserializes a `Secret<T>` from a URN string, producing an unbound secret.
189///
190/// The input must be a valid `urn:secrets-rs:<source_id>:<name>` string.
191/// Any other value is rejected with a descriptive error. The resulting secret
192/// must be bound via [`bind_all`](crate::bind_all) before its value can be accessed.
193impl<'de, T: SecretValue> Deserialize<'de> for Secret<T> {
194    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
195        let s = String::deserialize(deserializer)?;
196        let urn = s.parse::<Urn>().map_err(de::Error::custom)?;
197        Ok(Self { urn, value: None })
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::source::SourceRegistry;
205
206    #[test]
207    fn unbound_masked_value() {
208        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
209        assert_eq!(s.masked_value(), "urn:secrets-rs:env:MY_KEY [UNBOUND]");
210    }
211
212    #[test]
213    fn display_shows_masked_value() {
214        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
215        assert_eq!(s.to_string(), "urn:secrets-rs:env:MY_KEY [UNBOUND]");
216    }
217
218    #[test]
219    fn debug_shows_masked_value() {
220        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
221        assert_eq!(
222            format!("{s:?}"),
223            "Secret(urn:secrets-rs:env:MY_KEY [UNBOUND])"
224        );
225    }
226
227    #[test]
228    fn value_before_bind_is_error() {
229        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
230        assert!(s.value().is_err());
231    }
232
233    #[test]
234    fn bound_masked_value_includes_type_and_length() {
235        unsafe { std::env::set_var("SECRET_TEST_MASKED", "hello") };
236        let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_TEST_MASKED").unwrap();
237        let registry = SourceRegistry::new();
238        s.bind(&registry).unwrap();
239        assert_eq!(
240            s.masked_value(),
241            "urn:secrets-rs:env:SECRET_TEST_MASKED [string:5]"
242        );
243        unsafe { std::env::remove_var("SECRET_TEST_MASKED") };
244    }
245
246    #[test]
247    fn value_after_bind_returns_correct_value() {
248        unsafe { std::env::set_var("SECRET_TEST_VALUE", "s3cr3t") };
249        let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_TEST_VALUE").unwrap();
250        let registry = SourceRegistry::new();
251        s.bind(&registry).unwrap();
252        assert_eq!(s.value().unwrap(), "s3cr3t");
253        unsafe { std::env::remove_var("SECRET_TEST_VALUE") };
254    }
255
256    #[test]
257    fn serialize_produces_masked_string() {
258        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
259        let json = serde_json::to_string(&s).unwrap();
260        assert_eq!(json, r#""urn:secrets-rs:env:MY_KEY [UNBOUND]""#);
261    }
262
263    #[test]
264    fn deserialize_valid_urn_produces_unbound_secret() {
265        let s: Secret<String> = serde_json::from_str(r#""urn:secrets-rs:env:MY_KEY""#).unwrap();
266        assert_eq!(s.urn().to_string(), "urn:secrets-rs:env:MY_KEY");
267        assert!(s.value().is_err());
268    }
269
270    #[test]
271    fn deserialize_non_urn_string_errors() {
272        let result = serde_json::from_str::<Secret<String>>(r#""not-a-urn""#);
273        assert!(result.is_err());
274    }
275
276    #[test]
277    fn urn_returns_source_id_and_name() {
278        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
279        assert_eq!(s.urn().source_id, "env");
280        assert_eq!(s.urn().name, "MY_KEY");
281    }
282
283    #[test]
284    fn urn_is_unchanged_after_bind() {
285        unsafe { std::env::set_var("SECRET_URN_BIND_TEST", "value") };
286        let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_URN_BIND_TEST").unwrap();
287        let urn_before = s.urn().to_string();
288        let registry = SourceRegistry::new();
289        s.bind(&registry).unwrap();
290        assert_eq!(s.urn().to_string(), urn_before);
291        unsafe { std::env::remove_var("SECRET_URN_BIND_TEST") };
292    }
293
294    #[test]
295    fn urn_can_be_used_to_construct_second_unbound_secret() {
296        let original: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
297        let copy: Secret<String> = Secret::new(&original.urn().to_string()).unwrap();
298        assert_eq!(original.urn(), copy.urn());
299        assert!(copy.value().is_err());
300    }
301}