Skip to main content

use_config_secret/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when a secret reference is invalid.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct SecretRefError;
10
11impl fmt::Display for SecretRefError {
12    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
13        formatter.write_str("secret reference is empty")
14    }
15}
16
17impl Error for SecretRefError {}
18
19/// A safe reference to a secret value.
20#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
21pub struct SecretRef(String);
22
23impl SecretRef {
24    /// Creates a secret reference from a non-empty identifier.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`SecretRefError`] when the input is empty after trimming.
29    pub fn new(input: impl AsRef<str>) -> Result<Self, SecretRefError> {
30        let trimmed = input.as_ref().trim();
31
32        if trimmed.is_empty() {
33            Err(SecretRefError)
34        } else {
35            Ok(Self(trimmed.to_owned()))
36        }
37    }
38
39    /// Returns the safe secret reference identifier.
40    #[must_use]
41    pub fn id(&self) -> &str {
42        &self.0
43    }
44
45    /// Returns the owned secret reference identifier.
46    #[must_use]
47    pub fn into_string(self) -> String {
48        self.0
49    }
50}
51
52impl fmt::Debug for SecretRef {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        formatter.debug_tuple("SecretRef").field(&self.0).finish()
55    }
56}
57
58impl fmt::Display for SecretRef {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(formatter, "secret:{}", self.0)
61    }
62}
63
64impl FromStr for SecretRef {
65    type Err = SecretRefError;
66
67    fn from_str(input: &str) -> Result<Self, Self::Err> {
68        Self::new(input)
69    }
70}
71
72/// A value wrapper that redacts display and debug output.
73pub struct Redacted<T> {
74    value: T,
75}
76
77impl<T> Redacted<T> {
78    /// Wraps a value in redacted display/debug behavior.
79    #[must_use]
80    pub const fn new(value: T) -> Self {
81        Self { value }
82    }
83
84    /// Returns the wrapped value by reference.
85    #[must_use]
86    pub const fn expose(&self) -> &T {
87        &self.value
88    }
89
90    /// Returns the wrapped value by value.
91    #[must_use]
92    pub fn into_inner(self) -> T {
93        self.value
94    }
95}
96
97impl<T: Clone> Clone for Redacted<T> {
98    fn clone(&self) -> Self {
99        Self::new(self.value.clone())
100    }
101}
102
103impl<T: PartialEq> PartialEq for Redacted<T> {
104    fn eq(&self, other: &Self) -> bool {
105        self.value == other.value
106    }
107}
108
109impl<T: Eq> Eq for Redacted<T> {}
110
111impl<T> fmt::Debug for Redacted<T> {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        formatter.write_str("[redacted]")
114    }
115}
116
117impl<T> fmt::Display for Redacted<T> {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        formatter.write_str("[redacted]")
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::{Redacted, SecretRef, SecretRefError};
126
127    #[test]
128    fn debug_does_not_leak_wrapped_secret_value() {
129        let value = Redacted::new("very-secret-token");
130
131        assert_eq!(format!("{value:?}"), "[redacted]");
132        assert!(!format!("{value:?}").contains("very-secret-token"));
133    }
134
135    #[test]
136    fn display_does_not_leak_wrapped_secret_value() {
137        let value = Redacted::new("very-secret-token");
138
139        assert_eq!(value.to_string(), "[redacted]");
140        assert!(!value.to_string().contains("very-secret-token"));
141    }
142
143    #[test]
144    fn secret_reference_displays_safe_identifier() {
145        let reference = SecretRef::new(" database/password ").expect("reference should parse");
146
147        assert_eq!(reference.id(), "database/password");
148        assert_eq!(reference.to_string(), "secret:database/password");
149        assert_eq!(SecretRef::new(" "), Err(SecretRefError));
150    }
151
152    #[test]
153    fn equality_works_where_appropriate() {
154        let first = SecretRef::new("api/key").expect("reference should parse");
155        let second = SecretRef::new("api/key").expect("reference should parse");
156        let redacted_first = Redacted::new("same");
157        let redacted_second = Redacted::new("same");
158
159        assert_eq!(first, second);
160        assert_eq!(redacted_first, redacted_second);
161    }
162}