Skip to main content

alibi_core/config/
secrets.rs

1use crate::AuthError;
2use indexmap::IndexMap;
3
4/// Versioned persistence keys. New ciphertext uses the current version;
5/// retained versions read existing ciphertext without rewriting it. Removing
6/// a retained version retires it immediately. Signed cookies use only the
7/// current key and are invalidated when that key changes.
8#[derive(Clone)]
9pub struct ManagedSecrets {
10    current_version: u64,
11    keys: IndexMap<u64, String>,
12    legacy_secret: Option<String>,
13}
14
15impl ManagedSecrets {
16    #[must_use]
17    pub fn new(version: u64, secret: impl Into<String>) -> Self {
18        Self {
19            current_version: version,
20            keys: IndexMap::from([(version, secret.into())]),
21            legacy_secret: None,
22        }
23    }
24
25    /// Retain a previous version for reads. Replacing the current version's
26    /// key changes its key material; it does not change the selected version.
27    #[must_use]
28    pub fn retain(mut self, version: u64, secret: impl Into<String>) -> Self {
29        _ = self.keys.insert(version, secret.into());
30        self
31    }
32
33    /// Permit pre-managed bare ciphertext encrypted with this key.
34    #[must_use]
35    pub fn legacy(mut self, secret: impl Into<String>) -> Self {
36        self.legacy_secret = Some(secret.into());
37        self
38    }
39
40    /// Retire a reader. The current version cannot be retired; rotate by
41    /// constructing a new configuration with a new current version instead.
42    #[must_use]
43    pub fn retire(mut self, version: u64) -> Self {
44        if version != self.current_version {
45            _ = self.keys.shift_remove(&version);
46        }
47        self
48    }
49
50    #[must_use]
51    pub const fn current_version(&self) -> u64 {
52        self.current_version
53    }
54
55    #[must_use]
56    pub fn current_secret(&self) -> &str {
57        self.key(self.current_version).unwrap_or_default()
58    }
59
60    #[must_use]
61    pub fn key(&self, version: u64) -> Option<&str> {
62        self.keys.get(&version).map(String::as_str)
63    }
64
65    #[must_use]
66    pub fn legacy_secret(&self) -> Option<&str> {
67        self.legacy_secret.as_deref()
68    }
69
70    pub fn verification_secrets(&self) -> impl Iterator<Item = &str> {
71        self.keys.values().map(String::as_str).chain(
72            self.legacy_secret
73                .as_deref()
74                .filter(|legacy| !self.keys.values().any(|key| key == legacy)),
75        )
76    }
77
78    /// # Errors
79    /// Rejects invalid versions, empty readers and an insecure current key.
80    pub fn validate(&self) -> Result<(), AuthError> {
81        if self
82            .keys
83            .iter()
84            .any(|(version, key)| *version > 9_007_199_254_740_991 || key.is_empty())
85        {
86            return Err(AuthError::config(
87                "Secret versions must be safe nonnegative integers with nonempty keys",
88            ));
89        }
90        if self.current_secret().len() < 32 {
91            return Err(AuthError::config(
92                "Current secret key must be at least 32 characters",
93            ));
94        }
95        if self.legacy_secret.as_deref().is_some_and(str::is_empty) {
96            return Err(AuthError::config("Legacy secret key cannot be empty"));
97        }
98        Ok(())
99    }
100}
101
102impl std::fmt::Debug for ManagedSecrets {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("ManagedSecrets")
105            .field("current_version", &self.current_version)
106            .field("versions", &self.keys.keys())
107            .finish_non_exhaustive()
108    }
109}