gestura_core_security/
storage.rs

1//! Secure storage implementations for secrets management
2//!
3//! Provides abstractions for storing and retrieving sensitive data
4//! using either OS keychain integration or in-memory mock storage.
5
6use std::collections::HashMap;
7use thiserror::Error;
8
9/// Error type for secure storage operations
10#[derive(Debug, Error)]
11pub enum SecureStorageError {
12    /// Storage backend error
13    #[error("Storage error: {0}")]
14    Storage(String),
15
16    /// Key not found
17    #[error("Key not found: {0}")]
18    NotFound(String),
19
20    /// Lock poisoned
21    #[error("Lock poisoned")]
22    LockPoisoned,
23
24    /// I/O error
25    #[error("I/O error: {0}")]
26    Io(#[from] std::io::Error),
27}
28
29impl From<SecureStorageError> for gestura_core_foundation::AppError {
30    fn from(err: SecureStorageError) -> Self {
31        gestura_core_foundation::AppError::Io(std::io::Error::other(err.to_string()))
32    }
33}
34
35/// Secure storage interface for sensitive data
36///
37/// Implementations of this trait provide secure storage for secrets
38/// such as API keys, tokens, and encryption keys.
39#[async_trait::async_trait]
40pub trait SecureStorage: Send + Sync {
41    /// Store a secret value with the given key
42    async fn store_secret(&self, key: &str, value: &str) -> Result<(), SecureStorageError>;
43
44    /// Retrieve a secret value by key
45    async fn get_secret(&self, key: &str) -> Result<Option<String>, SecureStorageError>;
46
47    /// Delete a secret by key
48    async fn delete_secret(&self, key: &str) -> Result<(), SecureStorageError>;
49
50    /// Check if a secret exists
51    async fn has_secret(&self, key: &str) -> Result<bool, SecureStorageError> {
52        Ok(self.get_secret(key).await?.is_some())
53    }
54}
55
56/// Mock secure storage for testing
57///
58/// Stores secrets in memory without persistence. Suitable for testing
59/// and development environments where OS keychain is not available.
60pub struct MockSecureStorage {
61    data: std::sync::RwLock<HashMap<String, String>>,
62}
63
64impl Default for MockSecureStorage {
65    fn default() -> Self {
66        Self {
67            data: std::sync::RwLock::new(HashMap::new()),
68        }
69    }
70}
71
72impl MockSecureStorage {
73    /// Create a new mock storage instance
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Create a mock storage pre-populated with secrets
79    pub fn with_secrets(secrets: HashMap<String, String>) -> Self {
80        Self {
81            data: std::sync::RwLock::new(secrets),
82        }
83    }
84}
85
86#[async_trait::async_trait]
87impl SecureStorage for MockSecureStorage {
88    async fn store_secret(&self, key: &str, value: &str) -> Result<(), SecureStorageError> {
89        let mut data = self
90            .data
91            .write()
92            .map_err(|_| SecureStorageError::LockPoisoned)?;
93        data.insert(key.to_string(), value.to_string());
94        Ok(())
95    }
96
97    async fn get_secret(&self, key: &str) -> Result<Option<String>, SecureStorageError> {
98        let data = self
99            .data
100            .read()
101            .map_err(|_| SecureStorageError::LockPoisoned)?;
102        Ok(data.get(key).cloned())
103    }
104
105    async fn delete_secret(&self, key: &str) -> Result<(), SecureStorageError> {
106        let mut data = self
107            .data
108            .write()
109            .map_err(|_| SecureStorageError::LockPoisoned)?;
110        data.remove(key);
111        Ok(())
112    }
113}
114
115/// OS keychain integration (when security feature enabled)
116///
117/// Uses the operating system's secure credential storage:
118/// - macOS: Keychain
119/// - Windows: Credential Manager
120/// - Linux: Secret Service (via libsecret)
121#[cfg(feature = "security")]
122pub struct KeychainStorage;
123
124#[cfg(feature = "security")]
125#[async_trait::async_trait]
126impl SecureStorage for KeychainStorage {
127    async fn store_secret(&self, key: &str, value: &str) -> Result<(), SecureStorageError> {
128        let entry = keyring::Entry::new("gestura", key)
129            .map_err(|e| SecureStorageError::Storage(e.to_string()))?;
130        entry
131            .set_password(value)
132            .map_err(|e| SecureStorageError::Storage(e.to_string()))?;
133        Ok(())
134    }
135
136    async fn get_secret(&self, key: &str) -> Result<Option<String>, SecureStorageError> {
137        let entry = keyring::Entry::new("gestura", key)
138            .map_err(|e| SecureStorageError::Storage(e.to_string()))?;
139        match entry.get_password() {
140            Ok(password) => Ok(Some(password)),
141            Err(keyring::Error::NoEntry) => Ok(None),
142            Err(e) => Err(SecureStorageError::Storage(e.to_string())),
143        }
144    }
145
146    async fn delete_secret(&self, key: &str) -> Result<(), SecureStorageError> {
147        let entry = keyring::Entry::new("gestura", key)
148            .map_err(|e| SecureStorageError::Storage(e.to_string()))?;
149        match entry.delete_password() {
150            Ok(()) => Ok(()),
151            Err(keyring::Error::NoEntry) => Ok(()),
152            Err(e) => Err(SecureStorageError::Storage(e.to_string())),
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[tokio::test]
162    async fn test_mock_storage_store_and_get() {
163        let storage = MockSecureStorage::new();
164        storage
165            .store_secret("test_key", "test_value")
166            .await
167            .unwrap();
168        let value = storage.get_secret("test_key").await.unwrap();
169        assert_eq!(value, Some("test_value".to_string()));
170    }
171
172    #[tokio::test]
173    async fn test_mock_storage_delete() {
174        let storage = MockSecureStorage::new();
175        storage.store_secret("key", "value").await.unwrap();
176        storage.delete_secret("key").await.unwrap();
177        let value = storage.get_secret("key").await.unwrap();
178        assert!(value.is_none());
179    }
180
181    #[tokio::test]
182    async fn test_mock_storage_has_secret() {
183        let storage = MockSecureStorage::new();
184        assert!(!storage.has_secret("key").await.unwrap());
185        storage.store_secret("key", "value").await.unwrap();
186        assert!(storage.has_secret("key").await.unwrap());
187    }
188}