gestura_core_security/
secrets.rs1use crate::SecureStorage;
10use gestura_core_foundation::secrets::{SecretKey, SecretProvider};
11
12pub struct SecureStorageSecretProvider {
17 storage: Box<dyn SecureStorage>,
18}
19
20impl std::fmt::Debug for SecureStorageSecretProvider {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 f.debug_struct("SecureStorageSecretProvider")
23 .finish_non_exhaustive()
24 }
25}
26
27impl SecureStorageSecretProvider {
28 pub fn new(storage: Box<dyn SecureStorage>) -> Self {
30 Self { storage }
31 }
32}
33
34#[async_trait::async_trait]
35impl SecretProvider for SecureStorageSecretProvider {
36 async fn get_secret(&self, key: SecretKey) -> Option<String> {
37 let canonical_key = key.storage_key();
38
39 match self.storage.get_secret(canonical_key).await {
41 Ok(Some(v)) if !v.is_empty() => return Some(v),
42 Ok(_) => {}
43 Err(e) => {
44 tracing::warn!(
45 storage_key = canonical_key,
46 error = %e,
47 "Failed to read secret from secure storage"
48 );
49 }
50 }
51
52 let legacy_key = key.legacy_storage_key()?;
54 match self.storage.get_secret(legacy_key).await {
55 Ok(Some(v)) if !v.is_empty() => {
56 if let Err(e) = self.storage.store_secret(canonical_key, &v).await {
57 tracing::warn!(
58 canonical_key,
59 legacy_key,
60 error = %e,
61 "Failed to self-heal secret from legacy key to canonical key"
62 );
63 }
64 Some(v)
65 }
66 Ok(_) => None,
67 Err(e) => {
68 tracing::warn!(
69 storage_key = legacy_key,
70 error = %e,
71 "Failed to read secret from secure storage (legacy key)"
72 );
73 None
74 }
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::{MockSecureStorage, SecureStorageError};
83 use std::sync::Arc;
84
85 #[derive(Clone)]
89 struct SharedMockStorage(Arc<MockSecureStorage>);
90
91 #[async_trait::async_trait]
92 impl SecureStorage for SharedMockStorage {
93 async fn store_secret(&self, key: &str, value: &str) -> Result<(), SecureStorageError> {
94 self.0.store_secret(key, value).await
95 }
96
97 async fn get_secret(&self, key: &str) -> Result<Option<String>, SecureStorageError> {
98 self.0.get_secret(key).await
99 }
100
101 async fn delete_secret(&self, key: &str) -> Result<(), SecureStorageError> {
102 self.0.delete_secret(key).await
103 }
104 }
105
106 #[tokio::test]
107 async fn reads_legacy_key_and_self_heals_to_canonical() {
108 let inner = Arc::new(MockSecureStorage::new());
109 inner
110 .store_secret("gestura_api_key_openai", "sk-legacy")
111 .await
112 .unwrap();
113
114 let provider = SecureStorageSecretProvider::new(Box::new(SharedMockStorage(inner.clone())));
115
116 let got = provider.get_secret(SecretKey::OpenAi).await;
117 assert_eq!(got.as_deref(), Some("sk-legacy"));
118
119 let canonical = inner
120 .get_secret("gestura_llm_openai_api_key")
121 .await
122 .unwrap();
123 assert_eq!(canonical.as_deref(), Some("sk-legacy"));
124 }
125
126 #[tokio::test]
127 async fn canonical_key_wins_over_legacy_key() {
128 let inner = Arc::new(MockSecureStorage::new());
129 inner
130 .store_secret("gestura_llm_openai_api_key", "sk-canonical")
131 .await
132 .unwrap();
133 inner
134 .store_secret("gestura_api_key_openai", "sk-legacy")
135 .await
136 .unwrap();
137
138 let provider = SecureStorageSecretProvider::new(Box::new(SharedMockStorage(inner.clone())));
139
140 let got = provider.get_secret(SecretKey::OpenAi).await;
141 assert_eq!(got.as_deref(), Some("sk-canonical"));
142 }
143}