shuttle_common/
secrets.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    collections::{BTreeMap, HashMap},
4    fmt::Debug,
5};
6use zeroize::Zeroize;
7
8/// Wrapper type for secret values such as passwords or authentication keys.
9///
10/// Once wrapped, the inner value cannot leak accidentally, as both the [`std::fmt::Display`] and [`Debug`]
11/// implementations cover up the actual value and only show the type.
12///
13/// If you need access to the inner value, there is an [expose](`Secret::expose`) method.
14///
15/// To make sure nothing leaks after the [`Secret`] has been dropped, a custom [`Drop`]
16/// implementation will zero-out the underlying memory.
17#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = String, format = "password"))]
19pub struct Secret<T: Zeroize>(T);
20
21impl<T: Zeroize> Debug for Secret<T> {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "[REDACTED {:?}]", std::any::type_name::<T>())
24    }
25}
26
27impl<T: Zeroize> Drop for Secret<T> {
28    fn drop(&mut self) {
29        self.0.zeroize();
30    }
31}
32
33impl<T: Zeroize> From<T> for Secret<T> {
34    fn from(value: T) -> Self {
35        Self::new(value)
36    }
37}
38
39impl<T: Zeroize> Secret<T> {
40    pub fn new(secret: T) -> Self {
41        Self(secret)
42    }
43
44    /// Expose the underlying value of the secret
45    pub fn expose(&self) -> &T {
46        &self.0
47    }
48
49    /// Display a placeholder for the secret
50    pub fn redacted(&self) -> &str {
51        "********"
52    }
53}
54
55/// Store that holds all the secrets available to a deployment
56#[derive(Deserialize, Serialize, Clone)]
57#[serde(transparent)]
58pub struct SecretStore {
59    pub(crate) secrets: BTreeMap<String, Secret<String>>,
60}
61/// Helper type for typeshare
62#[allow(unused)]
63#[typeshare::typeshare]
64type SecretStoreT = HashMap<String, String>;
65
66impl SecretStore {
67    pub fn new(secrets: BTreeMap<String, Secret<String>>) -> Self {
68        Self { secrets }
69    }
70
71    pub fn get(&self, key: &str) -> Option<String> {
72        self.secrets.get(key).map(|s| s.expose().to_owned())
73    }
74}
75
76impl IntoIterator for SecretStore {
77    type Item = (String, String);
78    type IntoIter = <BTreeMap<String, String> as IntoIterator>::IntoIter;
79
80    fn into_iter(self) -> Self::IntoIter {
81        self.secrets
82            .into_iter()
83            .map(|(k, s)| (k, s.expose().to_owned()))
84            .collect::<BTreeMap<_, _>>()
85            .into_iter()
86    }
87}
88
89#[cfg(test)]
90#[allow(dead_code)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn redacted() {
96        let password_string = String::from("VERYSECRET");
97        let secret = Secret::new(password_string);
98        assert_eq!(secret.redacted(), "********");
99    }
100
101    #[test]
102    fn debug() {
103        let password_string = String::from("VERYSECRET");
104        let secret = Secret::new(password_string);
105        let printed = format!("{:?}", secret);
106        assert_eq!(printed, "[REDACTED \"alloc::string::String\"]");
107    }
108
109    #[test]
110    fn expose() {
111        let password_string = String::from("VERYSECRET");
112        let secret = Secret::new(password_string);
113        let printed = secret.expose();
114        assert_eq!(printed, "VERYSECRET");
115    }
116
117    #[test]
118    fn secret_struct() {
119        #[derive(Debug)]
120        struct Wrapper {
121            password: Secret<String>,
122        }
123
124        let password_string = String::from("VERYSECRET");
125        let secret = Secret::new(password_string);
126        let wrapper = Wrapper { password: secret };
127        let printed = format!("{:?}", wrapper);
128        assert_eq!(
129            printed,
130            "Wrapper { password: [REDACTED \"alloc::string::String\"] }"
131        );
132    }
133
134    #[test]
135    fn secretstore_intoiter() {
136        let bt = BTreeMap::from([
137            ("1".to_owned(), "2".to_owned().into()),
138            ("3".to_owned(), "4".to_owned().into()),
139        ]);
140        let ss = SecretStore::new(bt);
141
142        let mut iter = ss.into_iter();
143        assert_eq!(iter.next(), Some(("1".to_owned(), "2".to_owned())));
144        assert_eq!(iter.next(), Some(("3".to_owned(), "4".to_owned())));
145        assert_eq!(iter.next(), None);
146    }
147}