rustkernel_core/security/
secrets.rs

1//! Secure Secrets Management
2//!
3//! Provides secure storage and access to sensitive credentials.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10/// Reference to a secret (for configuration without exposing values)
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SecretRef {
13    /// Secret name
14    pub name: String,
15    /// Key within the secret (optional)
16    pub key: Option<String>,
17    /// Namespace or scope
18    pub namespace: Option<String>,
19}
20
21impl SecretRef {
22    /// Create a new secret reference
23    pub fn new(name: impl Into<String>) -> Self {
24        Self {
25            name: name.into(),
26            key: None,
27            namespace: None,
28        }
29    }
30
31    /// Specify a key within the secret
32    pub fn key(mut self, key: impl Into<String>) -> Self {
33        self.key = Some(key.into());
34        self
35    }
36
37    /// Specify namespace
38    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
39        self.namespace = Some(namespace.into());
40        self
41    }
42
43    /// Get the full path to the secret
44    pub fn path(&self) -> String {
45        let mut path = String::new();
46        if let Some(ref ns) = self.namespace {
47            path.push_str(ns);
48            path.push('/');
49        }
50        path.push_str(&self.name);
51        if let Some(ref key) = self.key {
52            path.push('/');
53            path.push_str(key);
54        }
55        path
56    }
57}
58
59/// Secret value (zeroized on drop when crypto feature is enabled)
60#[derive(Clone)]
61pub struct SecretValue {
62    value: Vec<u8>,
63}
64
65impl SecretValue {
66    /// Create a new secret value
67    pub fn new(value: impl Into<Vec<u8>>) -> Self {
68        Self {
69            value: value.into(),
70        }
71    }
72
73    /// Create from a string
74    pub fn from_string(s: impl Into<String>) -> Self {
75        Self {
76            value: s.into().into_bytes(),
77        }
78    }
79
80    /// Get the value as bytes
81    pub fn as_bytes(&self) -> &[u8] {
82        &self.value
83    }
84
85    /// Get the value as a string (if valid UTF-8)
86    pub fn as_str(&self) -> Option<&str> {
87        std::str::from_utf8(&self.value).ok()
88    }
89
90    /// Get the length
91    pub fn len(&self) -> usize {
92        self.value.len()
93    }
94
95    /// Check if empty
96    pub fn is_empty(&self) -> bool {
97        self.value.is_empty()
98    }
99}
100
101impl std::fmt::Debug for SecretValue {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        write!(f, "SecretValue([REDACTED, {} bytes])", self.value.len())
104    }
105}
106
107impl Drop for SecretValue {
108    fn drop(&mut self) {
109        // Zero out the secret value
110        for byte in &mut self.value {
111            *byte = 0;
112        }
113    }
114}
115
116/// Secret store trait
117pub trait SecretStore: Send + Sync {
118    /// Get a secret by name
119    fn get(&self, secret_ref: &SecretRef) -> Result<SecretValue, super::SecurityError>;
120
121    /// Set a secret
122    fn set(&self, secret_ref: &SecretRef, value: SecretValue) -> Result<(), super::SecurityError>;
123
124    /// Delete a secret
125    fn delete(&self, secret_ref: &SecretRef) -> Result<(), super::SecurityError>;
126
127    /// List secret names
128    fn list(&self, namespace: Option<&str>) -> Result<Vec<String>, super::SecurityError>;
129}
130
131/// In-memory secret store (for development/testing)
132#[derive(Default)]
133pub struct InMemorySecretStore {
134    secrets: Arc<RwLock<HashMap<String, SecretValue>>>,
135}
136
137impl InMemorySecretStore {
138    /// Create a new in-memory secret store
139    pub fn new() -> Self {
140        Self {
141            secrets: Arc::new(RwLock::new(HashMap::new())),
142        }
143    }
144
145    /// Get a secret (async version)
146    pub async fn get_async(
147        &self,
148        secret_ref: &SecretRef,
149    ) -> Result<SecretValue, super::SecurityError> {
150        let secrets = self.secrets.read().await;
151        secrets.get(&secret_ref.path()).cloned().ok_or_else(|| {
152            super::SecurityError::SecretNotFound {
153                name: secret_ref.path(),
154            }
155        })
156    }
157
158    /// Set a secret (async version)
159    pub async fn set_async(
160        &self,
161        secret_ref: &SecretRef,
162        value: SecretValue,
163    ) -> Result<(), super::SecurityError> {
164        let mut secrets = self.secrets.write().await;
165        secrets.insert(secret_ref.path(), value);
166        Ok(())
167    }
168
169    /// Delete a secret (async version)
170    pub async fn delete_async(&self, secret_ref: &SecretRef) -> Result<(), super::SecurityError> {
171        let mut secrets = self.secrets.write().await;
172        secrets.remove(&secret_ref.path());
173        Ok(())
174    }
175
176    /// List secrets (async version)
177    pub async fn list_async(
178        &self,
179        namespace: Option<&str>,
180    ) -> Result<Vec<String>, super::SecurityError> {
181        let secrets = self.secrets.read().await;
182        let names: Vec<String> = secrets
183            .keys()
184            .filter(|k| {
185                namespace
186                    .map(|ns| k.starts_with(&format!("{}/", ns)))
187                    .unwrap_or(true)
188            })
189            .cloned()
190            .collect();
191        Ok(names)
192    }
193}
194
195/// Environment variable secret store
196pub struct EnvSecretStore {
197    prefix: Option<String>,
198}
199
200impl EnvSecretStore {
201    /// Create a new env secret store
202    pub fn new() -> Self {
203        Self { prefix: None }
204    }
205
206    /// Set a prefix for environment variables
207    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
208        self.prefix = Some(prefix.into());
209        self
210    }
211
212    /// Get the env var name for a secret
213    fn env_name(&self, secret_ref: &SecretRef) -> String {
214        let name = secret_ref.name.to_uppercase().replace(['-', '/'], "_");
215
216        match &self.prefix {
217            Some(prefix) => format!("{}_{}", prefix.to_uppercase(), name),
218            None => name,
219        }
220    }
221
222    /// Get a secret
223    pub fn get(&self, secret_ref: &SecretRef) -> Result<SecretValue, super::SecurityError> {
224        let env_name = self.env_name(secret_ref);
225        std::env::var(&env_name)
226            .map(SecretValue::from_string)
227            .map_err(|_| super::SecurityError::SecretNotFound {
228                name: secret_ref.path(),
229            })
230    }
231}
232
233impl Default for EnvSecretStore {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_secret_ref() {
245        let secret_ref = SecretRef::new("database-password")
246            .namespace("prod")
247            .key("password");
248
249        assert_eq!(secret_ref.path(), "prod/database-password/password");
250    }
251
252    #[test]
253    fn test_secret_value() {
254        let secret = SecretValue::from_string("super-secret");
255        assert_eq!(secret.as_str(), Some("super-secret"));
256        assert_eq!(secret.len(), 12);
257    }
258
259    #[test]
260    fn test_secret_value_debug() {
261        let secret = SecretValue::from_string("super-secret");
262        let debug = format!("{:?}", secret);
263        assert!(!debug.contains("super-secret"));
264        assert!(debug.contains("REDACTED"));
265    }
266
267    #[tokio::test]
268    async fn test_in_memory_store() {
269        let store = InMemorySecretStore::new();
270        let secret_ref = SecretRef::new("test-secret");
271        let value = SecretValue::from_string("test-value");
272
273        store.set_async(&secret_ref, value).await.unwrap();
274
275        let retrieved = store.get_async(&secret_ref).await.unwrap();
276        assert_eq!(retrieved.as_str(), Some("test-value"));
277
278        store.delete_async(&secret_ref).await.unwrap();
279        assert!(store.get_async(&secret_ref).await.is_err());
280    }
281
282    #[test]
283    fn test_env_secret_store_name() {
284        let store = EnvSecretStore::new().with_prefix("RUSTKERNEL");
285        let secret_ref = SecretRef::new("database-password");
286
287        assert_eq!(store.env_name(&secret_ref), "RUSTKERNEL_DATABASE_PASSWORD");
288    }
289}