Mountain/Environment/SecretProvider.rs
1//! # SecretProvider (Environment)
2//!
3//! Implements the `SecretProvider` trait for `MountainEnvironment`, providing
4//! secure storage and retrieval of secrets (passwords, tokens, keys) with
5//! optional integration with system keychains and the Air service.
6//!
7//! ## RESPONSIBILITIES
8//!
9//! ### 1. Secret Storage
10//! - Store secrets securely in encrypted format
11//! - Support multiple secret types (passwords, API keys, tokens)
12//! - Provide per-secret access control and metadata
13//! - Handle secret creation, update, and deletion
14//!
15//! ### 2. Secret Retrieval
16//! - Retrieve stored secrets by key
17//! - Cache frequently accessed secrets for performance
18//! - Support secret resolution with fallbacks
19//! - Handle missing or expired secrets gracefully
20//!
21//! ### 3. Security
22//! - Encrypt secrets at rest using strong cryptography
23//! - Optional integration with system keychain (macOS Keychain, Windows DPAPI,
24//! etc.)
25//! - Secure memory handling for secret values
26//! - Audit logging for secret access (optional)
27//!
28//! ### 4. Air Integration (Optional)
29//! - Delegate secret storage to Air service when available
30//! - Support cloud-synced secrets across devices
31//! - Handle Air service availability failures with fallback
32//!
33//! ## ARCHITECTURAL ROLE
34//!
35//! SecretProvider is the **secure credential manager** for Mountain:
36//!
37//! ```text
38//! Provider ──► Store/Retrieve ──► Secret Storage (Local or Air)
39//! ```
40//!
41//! ### Position in Mountain
42//! - `Environment` module: Security capability provider
43//! - Implements `CommonLibrary::Secret::SecretProvider` trait
44//! - Accessible via `Environment.Require<dyn SecretProvider>()`
45//!
46//! ### Secret Storage Backends
47//! - **Local Storage**: Encrypted file in app data directory (default)
48//! - **System Keychain**: Platform-native secure storage (optional)
49//! - **Air Service**: Cloud-based secret management (optional, feature-gated)
50//!
51//! ### Dependencies
52//! - `ApplicationState`: For storage paths and state
53//! - `ConfigurationProvider`: To read security settings
54//! - `Log`: Secret access auditing (if enabled)
55//!
56//! ### Dependents
57//! - Authentication flows: Store and retrieve OAuth tokens
58//! - Git credentials: Store SCM passwords and tokens
59//! - Extension secrets: Extension-specific API keys
60//! - System secrets: Mountain service account credentials
61//!
62//! ## SECURITY CONSIDERATIONS
63//!
64//! - Secrets are never logged or exposed in error messages
65//! - Secret values are zeroed from memory after use
66//! - Access to secret storage should be audited
67//! - Consider rate limiting secret retrieval attempts
68//! - Implement secret expiration and rotation policies
69//!
70//! ## PERFORMANCE
71//!
72//! - Secret lookups are cached to avoid repeated decryption
73//! - Async operations to avoid blocking the UI
74//! - Consider lazy loading for rarely used secrets
75//!
76//! ## VS CODE REFERENCE
77//!
78//! Patterns from VS Code:
79//! - `vs/platform/secrets/common/secrets.ts` - Secret storage API
80//! - `vs/platform/secrets/electron-simulator/electronSecretStorage.ts` -
81//! Keychain integration
82//!
83//! ## TODO
84//!
85//! - [ ] Implement system keychain integration (macOS Keychain, Windows DPAPI,
86//! libsecret)
87//! - [ ] Add secret encryption with hardware-backed keys (TPM, Secure Enclave)
88//! - [ ] Implement secret versioning and history
89//! - [ ] Add secret access control lists (ACL) per provider
90//! - [ ] Support secret sharing between extensions
91//! - [ ] Implement secret backup and restore
92//! - [ ] Add secret expiration and automatic rotation
93//! - [ ] Support secret references (pointer to external secret)
94//! - [ ] Implement secret audit trail and compliance reporting
95//! - [ ] Add secret strength validation and generation
96//!
97//! ## MODULE CONTENTS
98//!
99//! - [`SecretProvider`]: Main struct implementing the trait
100//! - Secret storage and retrieval methods
101//! - Encryption/decryption helpers
102//! - System keychain abstraction
103//! - Air service delegation logic
104
105// Responsibilities:
106// - Securely store and retrieve secrets using the OS keychain.
107// - Provide a consistent API across platforms (Windows, macOS, Linux).
108// - Handle keychain access failures gracefully with proper error handling.
109// - Support secret sharing between processes via unique service names.
110// - Integrate with Air service for cloud synchronization (optional).
111// - Ensure secrets are never exposed in logs or error messages.
112// - Provide secure secret storage with encryption.
113// - Handle secret lifecycle (create, read, update, delete).
114//
115// TODOs:
116// - Implement complete Air-based secret storage
117// - Add secret sync between Air and local keyring
118// - Implement conflict resolution strategies for sync
119// - Add caching layer for frequently accessed secrets
120// - Implement retry logic for transient keychain failures
121// - Add metrics for Air vs Local usage tracking
122// - Implement secret versioning (for rollback capability)
123// - Add secret expiration support
124// - Implement secret audit logging
125// - Support secret encryption at rest for additional security
126// - Add secret backup and recovery
127// - Implement secret migration utilities
128// - Add secret access control and permissions
129// - Support secret sharing between devices (via Air)
130// - Implement secret key derivation (PBKDF2, scrypt)
131// - Add secret validation and integrity checking
132//
133// Inspired by VSCode's secrets service which:
134// - Uses operating system keychain for secure storage
135// - Provides consistent API across platforms (macOS Keychain, Windows Credential Manager, Linux Secret Service)
136// - Handles keychain access failures gracefully
137// - Supports secret encryption
138// - Provides secure secret sharing between processes
139//! # SecretProvider Implementation
140//!
141//! Implements the `SecretProvider` trait for the `MountainEnvironment`. This
142//! provider contains the core logic for secure secret storage using the system
143//! keyring, powered by the `keyring` crate.
144//!
145//! ## Keyring Integration
146//!
147//! The `keyring` crate provides cross-platform secure storage:
148//! - **macOS**: Native Keychain (OSXKeychain)
149//! - **Windows**: Windows Credential Manager (WinCredential)
150//! - **Linux**: Secret Service API (dbus-secret-service) or GNOME Keyring
151//!
152//! Each secret is identified by:
153//! - **Service Name**: Application identifier (e.g., `com.myapp.mountain`)
154//! - **Key**: Unique identifier within the service (e.g., `github-token`)
155//! - **Value**: The secret data to store
156//!
157//! ## Security Considerations
158//!
159//! 1. **No Secret Logging**: Secrets are never logged or included in error
160//! messages
161//! 2. **Secure Storage**: Keyring handles encryption at the OS level
162//! 3. **Access Control**: OS keychain manages access permissions and unlocking
163//! 4. **Error Handling**: Failed operations don't expose secret values
164//! 5. **Input Validation**: Extension and key identifiers are validated
165//!
166//! ## Air Integration Strategy
167//!
168//! This provider supports delegation to the Air service when available:
169//! - If AirClient is provided, secrets are stored/retrieved via Air service
170//! - If AirClient is unavailable, falls back to local keyring implementation
171//! - This ensures backward compatibility while enabling cloud sync
172//! - Health checks determine Air availability at runtime
173//!
174//! ## Secret Operations
175//!
176//! - **GetSecret**: Retrieve a secret from storage
177//! - Returns `Some(Value)` if found, `None` if not found
178//! - Delegates to Air if available and healthy
179//! - Falls back to local keyring otherwise
180//!
181//! - **StoreSecret**: Store or update a secret
182//! - Creates entry if it doesn't exist
183//! - Updates entry if it already exists
184//! - Delegates to Air if available and healthy
185//! - Falls back to local keyring otherwise
186//!
187//! - **DeleteSecret**: Remove a secret from storage
188//! - Succeeds even if secret doesn't exist
189//! - Delegates to Air if available and healthy
190//! - Falls back to local keyring otherwise
191// TODO: Full Air Migration Plan
192// ============================
193// - [ ] Implement complete Air-based secret storage and retrieval, replacing
194// local keyring calls with Air service RPCs for all operations
195// - [ ] Add secret synchronization between Air and local keyring for offline
196// mode and gradual migration support. Use version vectors or timestamps for
197// conflict detection and implement last-write-wins or manual merge strategies
198// - [ ] Implement conflict resolution strategies for concurrent secret updates
199// from multiple sources (Air vs local, different extensions). Provide UI for
200// user to resolve conflicts when automatic resolution is not possible
201// - [ ] Add caching layer (in-memory LRU or ttl cache) for frequently accessed
202// secrets to reduce latency and Air service load. Invalidate on secret
203// updates.
204// - [ ] Implement retry logic with exponential backoff for transient Air
205// service failures. Circuit breaker pattern to prevent cascading failures
206// during outages
207// - [ ] Add metrics collection for Air vs Local usage tracking, latency
208// percentiles, error rates, and cache hit rates to inform deployment
209// decisions
210// - [ ] Phase out local keyring after successful Air deployment and validation
211// period (e.g., 2 weeks of stable operation). Keep fallback for Air
212// unavailability
213
214use CommonLibrary::{Error::CommonError::CommonError, Secret::SecretProvider::SecretProvider};
215use async_trait::async_trait;
216use keyring::Entry;
217// Import Air client types when Air is available in the workspace
218#[cfg(feature = "AirIntegration")]
219use AirLibrary::Vine::Generated::air::air_service_client::AirServiceClient;
220
221use super::MountainEnvironment::MountainEnvironment;
222use crate::dev_log;
223
224/// Constructs the service name for the keyring entry.
225fn GetKeyringServiceName(Environment:&MountainEnvironment, ExtensionIdentifier:&str) -> String {
226 format!("{}.{}", Environment.ApplicationHandle.package_info().name, ExtensionIdentifier)
227}
228
229/// Helper to check if Air client is available and healthy.
230#[cfg(feature = "AirIntegration")]
231async fn IsAirAvailable(_AirClient:&AirServiceClient<tonic::transport::Channel>) -> bool {
232 // TODO: Implement proper health check when AirClient wrapper is available
233 // The raw gRPC client requires &mut self for health_check, but
234 // MountainEnvironment stores an immutable reference. This will be fixed when
235 // the AirClient wrapper is properly integrated.
236 // For now, assume Air is available if the client exists
237 true
238}
239
240#[async_trait]
241impl SecretProvider for MountainEnvironment {
242 /// Retrieves a secret by reading from the OS keychain.
243 /// If Air is available and healthy, delegates to Air service.
244 /// Falls back to local keyring if Air is unavailable.
245 #[allow(unused_mut, unused_variables)]
246 async fn GetSecret(&self, ExtensionIdentifier:String, Key:String) -> Result<Option<String>, CommonError> {
247 dev_log!(
248 "storage",
249 "[SecretProvider] Getting secret for ext: '{}', key: '{}'",
250 ExtensionIdentifier,
251 Key
252 );
253
254 #[cfg(feature = "AirIntegration")]
255 {
256 if let Some(AirClient) = &self.AirClient {
257 if IsAirAvailable(AirClient).await {
258 dev_log!(
259 "storage",
260 "[SecretProvider] Delegating GetSecret to Air service for key: '{}'",
261 Key
262 );
263
264 return GetSecretFromAir(AirClient, ExtensionIdentifier.clone(), Key).await;
265 } else {
266 dev_log!(
267 "storage",
268 "warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
269 Key
270 );
271 }
272 }
273 }
274
275 dev_log!(
276 "storage",
277 "[SecretProvider] Using local keyring for ext: '{}'",
278 ExtensionIdentifier
279 );
280
281 let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
282
283 let Entry = Entry::new(&ServiceName, &Key)
284 .map_err(|Error| CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() })?;
285
286 match Entry.get_password() {
287 Ok(Password) => Ok(Some(Password)),
288
289 Err(keyring::Error::NoEntry) => Ok(None),
290
291 Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
292 }
293 }
294
295 /// Stores a secret by writing to the OS keychain.
296 /// If Air is available and healthy, delegates to Air service.
297 /// Falls back to local keyring if Air is unavailable.
298 #[allow(unused_mut, unused_variables)]
299 async fn StoreSecret(&self, ExtensionIdentifier:String, Key:String, Value:String) -> Result<(), CommonError> {
300 dev_log!(
301 "storage",
302 "[SecretProvider] Storing secret for ext: '{}', key: '{}'",
303 ExtensionIdentifier,
304 Key
305 );
306
307 #[cfg(feature = "AirIntegration")]
308 {
309 if let Some(AirClient) = &self.AirClient {
310 if IsAirAvailable(AirClient).await {
311 dev_log!(
312 "storage",
313 "[SecretProvider] Delegating StoreSecret to Air service for key: '{}'",
314 Key
315 );
316
317 return StoreSecretToAir(AirClient, ExtensionIdentifier.clone(), Key, Value).await;
318 } else {
319 dev_log!(
320 "storage",
321 "warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
322 Key
323 );
324 }
325 }
326 }
327
328 dev_log!(
329 "storage",
330 "[SecretProvider] Using local keyring for ext: '{}'",
331 ExtensionIdentifier
332 );
333
334 let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
335
336 let Entry = Entry::new(&ServiceName, &Key)
337 .map_err(|Error| CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() })?;
338
339 Entry
340 .set_password(&Value)
341 .map_err(|Error| CommonError::SecretsAccess { Key, Reason:Error.to_string() })
342 }
343
344 /// Deletes a secret by removing it from the OS keychain.
345 /// If Air is available and healthy, delegates to Air service.
346 /// Falls back to local keyring if Air is unavailable.
347 #[allow(unused_mut, unused_variables)]
348 async fn DeleteSecret(&self, ExtensionIdentifier:String, Key:String) -> Result<(), CommonError> {
349 dev_log!(
350 "storage",
351 "[SecretProvider] Deleting secret for ext: '{}', key: '{}'",
352 ExtensionIdentifier,
353 Key
354 );
355
356 #[cfg(feature = "AirIntegration")]
357 {
358 if let Some(AirClient) = &self.AirClient {
359 if IsAirAvailable(AirClient).await {
360 dev_log!(
361 "storage",
362 "[SecretProvider] Delegating DeleteSecret to Air service for key: '{}'",
363 Key
364 );
365
366 return DeleteSecretFromAir(AirClient, ExtensionIdentifier.clone(), Key).await;
367 } else {
368 dev_log!(
369 "storage",
370 "warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
371 Key
372 );
373 }
374 }
375 }
376
377 dev_log!(
378 "storage",
379 "[SecretProvider] Using local keyring for ext: '{}'",
380 ExtensionIdentifier
381 );
382
383 let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
384
385 let Entry = Entry::new(&ServiceName, &Key)
386 .map_err(|Error| CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() })?;
387
388 match Entry.delete_credential() {
389 Ok(_) | Err(keyring::Error::NoEntry) => Ok(()),
390
391 Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
392 }
393 }
394}
395
396// ============================================================================
397// Air Integration Functions
398// ============================================================================
399
400/// Retrieves a secret from the Air service.
401#[cfg(feature = "AirIntegration")]
402async fn GetSecretFromAir(
403 _AirClient:&AirServiceClient<tonic::transport::Channel>,
404 ExtensionIdentifier:String,
405 Key:String,
406) -> Result<Option<String>, CommonError> {
407 dev_log!(
408 "storage",
409 "[SecretProvider] Fetching secret from Air: ext='{}', key='{}'",
410 ExtensionIdentifier,
411 Key
412 );
413
414 // TODO: Implement Air secret retrieval by calling the Air service's GetSecret
415 // RPC method. This should:
416 // - Construct a GetSecretRequest with ExtensionIdentifier and Key
417 // - Call AirClient.get_secret (or similar) with appropriate timeout
418 // - Map Air service errors to CommonError (NotFound, AccessDenied, etc.)
419 // - Return Ok(Some(secret)) if found, Ok(None) if not found
420 // The Air service provides centralized secret storage with audit logging,
421 // access control, and cross-device sync capabilities.
422 Err(CommonError::NotImplemented { FeatureName:"GetSecretFromAir".to_string() })
423}
424
425/// Stores a secret in the Air service.
426#[cfg(feature = "AirIntegration")]
427async fn StoreSecretToAir(
428 _AirClient:&AirServiceClient<tonic::transport::Channel>,
429 ExtensionIdentifier:String,
430 Key:String,
431 _Value:String,
432) -> Result<(), CommonError> {
433 dev_log!(
434 "storage",
435 "[SecretProvider] Storing secret in Air: ext='{}', key='{}'",
436 ExtensionIdentifier,
437 Key
438 );
439
440 // TODO: Implement Air secret storage by calling the Air service's StoreSecret
441 // RPC method. This should:
442 // - Construct a StoreSecretRequest with ExtensionIdentifier, Key, and Value
443 // - Call AirClient.store_secret (or similar) with the secret payload
444 // - Handle encryption and secure transmission to the Air service
445 // - Return Ok(()) on success, map errors to CommonError appropriately
446 // The Air service handles secret encryption at rest and provides fine-grained
447 // access control and versioning for secret updates.
448 Err(CommonError::NotImplemented { FeatureName:"StoreSecretToAir".to_string() })
449}
450
451/// Deletes a secret from the Air service.
452#[cfg(feature = "AirIntegration")]
453async fn DeleteSecretFromAir(
454 _AirClient:&AirServiceClient<tonic::transport::Channel>,
455 ExtensionIdentifier:String,
456 Key:String,
457) -> Result<(), CommonError> {
458 dev_log!(
459 "storage",
460 "[SecretProvider] Deleting secret from Air: ext='{}', key='{}'",
461 ExtensionIdentifier,
462 Key
463 );
464
465 // TODO: Implement Air secret deletion by calling the Air service's DeleteSecret
466 // RPC method. This should:
467 // - Construct a DeleteSecretRequest with ExtensionIdentifier and Key
468 // - Call AirClient.delete_secret (or similar) to remove the secret
469 // - Handle idempotency: deleting a non-existent secret should succeed
470 // - Return Ok(()) on success, map errors to CommonError as needed
471 // The Air service ensures secure deletion and propagates changes to other
472 // devices via sync, maintaining consistency across the user's ecosystem.
473 Err(CommonError::NotImplemented { FeatureName:"DeleteSecretFromAir".to_string() })
474}