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 underlying URN.
101    pub fn urn(&self) -> &Urn {
102        &self.urn
103    }
104
105    /// Returns the secret value, or an [`UnboundError`] if not yet bound.
106    pub fn value(&self) -> Result<&T, UnboundError> {
107        self.value.as_ref().ok_or_else(|| UnboundError {
108            urn: self.urn.to_string(),
109        })
110    }
111
112    /// Returns the masked value string — safe to log or serialize by default.
113    pub fn masked_value(&self) -> String {
114        match &self.value {
115            None => format!("{} [UNBOUND]", self.urn),
116            Some(v) => format!("{} [{}:{}]", self.urn, T::type_name(), v.masked_size()),
117        }
118    }
119
120    /// Fetches the secret from the appropriate source in `registry` and stores
121    /// it. Returns [`BindError`] if the source is not registered or the lookup
122    /// fails.
123    pub fn bind(&mut self, registry: &SourceRegistry) -> Result<(), BindError> {
124        let urn_str = self.urn.to_string();
125        let source =
126            registry
127                .get(&self.urn.source_id)
128                .ok_or_else(|| BindError::SourceNotFound {
129                    source_id: self.urn.source_id.clone(),
130                })?;
131
132        let bytes = source.get(&self.urn.name).map_err(|e| {
133            use crate::error::SourceError;
134            match e {
135                SourceError::NotFound { name } => BindError::NameNotFound {
136                    source_id: self.urn.source_id.clone(),
137                    name,
138                },
139                other => BindError::Source {
140                    urn: urn_str.clone(),
141                    source: other,
142                },
143            }
144        })?;
145
146        self.value = Some(T::from_bytes(bytes, &urn_str)?);
147        Ok(())
148    }
149}
150
151impl<T: SecretValue> fmt::Display for Secret<T> {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        f.write_str(&self.masked_value())
154    }
155}
156
157/// Always displays the masked value — the real value is never revealed via Debug.
158impl<T: SecretValue> fmt::Debug for Secret<T> {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        write!(f, "Secret({})", self.masked_value())
161    }
162}
163
164impl<T: SecretValue> Serialize for Secret<T> {
165    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
166        serializer.serialize_str(&self.masked_value())
167    }
168}
169
170/// Deserializes a `Secret<T>` from a URN string, producing an unbound secret.
171///
172/// The input must be a valid `urn:secrets-rs:<source_id>:<name>` string.
173/// Any other value is rejected with a descriptive error. The resulting secret
174/// must be bound via [`bind_all`](crate::bind_all) before its value can be accessed.
175impl<'de, T: SecretValue> Deserialize<'de> for Secret<T> {
176    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
177        let s = String::deserialize(deserializer)?;
178        let urn = s.parse::<Urn>().map_err(de::Error::custom)?;
179        Ok(Self { urn, value: None })
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::source::SourceRegistry;
187    use crate::sources::env::EnvSource;
188
189    #[test]
190    fn unbound_masked_value() {
191        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
192        assert_eq!(s.masked_value(), "urn:secrets-rs:env:MY_KEY [UNBOUND]");
193    }
194
195    #[test]
196    fn display_shows_masked_value() {
197        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
198        assert_eq!(s.to_string(), "urn:secrets-rs:env:MY_KEY [UNBOUND]");
199    }
200
201    #[test]
202    fn debug_shows_masked_value() {
203        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
204        assert_eq!(
205            format!("{s:?}"),
206            "Secret(urn:secrets-rs:env:MY_KEY [UNBOUND])"
207        );
208    }
209
210    #[test]
211    fn value_before_bind_is_error() {
212        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
213        assert!(s.value().is_err());
214    }
215
216    #[test]
217    fn bound_masked_value_includes_type_and_length() {
218        unsafe { std::env::set_var("SECRET_TEST_MASKED", "hello") };
219        let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_TEST_MASKED").unwrap();
220        let mut registry = SourceRegistry::new();
221        registry.register("env", EnvSource);
222        s.bind(&registry).unwrap();
223        assert_eq!(
224            s.masked_value(),
225            "urn:secrets-rs:env:SECRET_TEST_MASKED [string:5]"
226        );
227        unsafe { std::env::remove_var("SECRET_TEST_MASKED") };
228    }
229
230    #[test]
231    fn value_after_bind_returns_correct_value() {
232        unsafe { std::env::set_var("SECRET_TEST_VALUE", "s3cr3t") };
233        let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_TEST_VALUE").unwrap();
234        let mut registry = SourceRegistry::new();
235        registry.register("env", EnvSource);
236        s.bind(&registry).unwrap();
237        assert_eq!(s.value().unwrap(), "s3cr3t");
238        unsafe { std::env::remove_var("SECRET_TEST_VALUE") };
239    }
240
241    #[test]
242    fn serialize_produces_masked_string() {
243        let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
244        let json = serde_json::to_string(&s).unwrap();
245        assert_eq!(json, r#""urn:secrets-rs:env:MY_KEY [UNBOUND]""#);
246    }
247
248    #[test]
249    fn deserialize_valid_urn_produces_unbound_secret() {
250        let s: Secret<String> = serde_json::from_str(r#""urn:secrets-rs:env:MY_KEY""#).unwrap();
251        assert_eq!(s.urn().to_string(), "urn:secrets-rs:env:MY_KEY");
252        assert!(s.value().is_err());
253    }
254
255    #[test]
256    fn deserialize_non_urn_string_errors() {
257        let result = serde_json::from_str::<Secret<String>>(r#""not-a-urn""#);
258        assert!(result.is_err());
259    }
260}