rustkernel_core/security/
secrets.rs1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SecretRef {
13 pub name: String,
15 pub key: Option<String>,
17 pub namespace: Option<String>,
19}
20
21impl SecretRef {
22 pub fn new(name: impl Into<String>) -> Self {
24 Self {
25 name: name.into(),
26 key: None,
27 namespace: None,
28 }
29 }
30
31 pub fn key(mut self, key: impl Into<String>) -> Self {
33 self.key = Some(key.into());
34 self
35 }
36
37 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
39 self.namespace = Some(namespace.into());
40 self
41 }
42
43 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#[derive(Clone)]
61pub struct SecretValue {
62 value: Vec<u8>,
63}
64
65impl SecretValue {
66 pub fn new(value: impl Into<Vec<u8>>) -> Self {
68 Self {
69 value: value.into(),
70 }
71 }
72
73 pub fn from_string(s: impl Into<String>) -> Self {
75 Self {
76 value: s.into().into_bytes(),
77 }
78 }
79
80 pub fn as_bytes(&self) -> &[u8] {
82 &self.value
83 }
84
85 pub fn as_str(&self) -> Option<&str> {
87 std::str::from_utf8(&self.value).ok()
88 }
89
90 pub fn len(&self) -> usize {
92 self.value.len()
93 }
94
95 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 for byte in &mut self.value {
111 *byte = 0;
112 }
113 }
114}
115
116pub trait SecretStore: Send + Sync {
118 fn get(&self, secret_ref: &SecretRef) -> Result<SecretValue, super::SecurityError>;
120
121 fn set(&self, secret_ref: &SecretRef, value: SecretValue) -> Result<(), super::SecurityError>;
123
124 fn delete(&self, secret_ref: &SecretRef) -> Result<(), super::SecurityError>;
126
127 fn list(&self, namespace: Option<&str>) -> Result<Vec<String>, super::SecurityError>;
129}
130
131#[derive(Default)]
133pub struct InMemorySecretStore {
134 secrets: Arc<RwLock<HashMap<String, SecretValue>>>,
135}
136
137impl InMemorySecretStore {
138 pub fn new() -> Self {
140 Self {
141 secrets: Arc::new(RwLock::new(HashMap::new())),
142 }
143 }
144
145 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 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 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 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
195pub struct EnvSecretStore {
197 prefix: Option<String>,
198}
199
200impl EnvSecretStore {
201 pub fn new() -> Self {
203 Self { prefix: None }
204 }
205
206 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
208 self.prefix = Some(prefix.into());
209 self
210 }
211
212 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 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}