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 std::sync::Arc;
215
216use CommonLibrary::{Error::CommonError::CommonError, Secret::SecretProvider::SecretProvider};
217use async_trait::async_trait;
218use keyring::Entry;
219use log::{info, trace, warn};
220// Import Air client types when Air is available in the workspace
221#[cfg(feature = "AirIntegration")]
222use AirLibrary::Vine::Generated::air::air_service_client::AirServiceClient;
223#[cfg(feature = "AirIntegration")]
224use AirLibrary::Vine::Generated::air::HealthCheckRequest;
225
226use super::MountainEnvironment::MountainEnvironment;
227
228/// Constructs the service name for the keyring entry.
229fn GetKeyringServiceName(Environment:&MountainEnvironment, ExtensionIdentifier:&str) -> String {
230 format!("{}.{}", Environment.ApplicationHandle.package_info().name, ExtensionIdentifier)
231}
232
233/// Helper to check if Air client is available and healthy.
234#[cfg(feature = "AirIntegration")]
235async fn IsAirAvailable(_AirClient:&AirServiceClient<tonic::transport::Channel>) -> bool {
236 // TODO: Implement proper health check when AirClient wrapper is available
237 // The raw gRPC client requires &mut self for health_check, but MountainEnvironment
238 // stores an immutable reference. This will be fixed when the AirClient wrapper
239 // is properly integrated.
240 // For now, assume Air is available if the client exists
241 true
242}
243
244#[async_trait]
245impl SecretProvider for MountainEnvironment {
246 /// Retrieves a secret by reading from the OS keychain.
247 /// If Air is available and healthy, delegates to Air service.
248 /// Falls back to local keyring if Air is unavailable.
249 #[allow(unused_mut, unused_variables)]
250 async fn GetSecret(&self, ExtensionIdentifier:String, Key:String) -> Result<Option<String>, CommonError> {
251 trace!(
252 "[SecretProvider] Getting secret for ext: '{}', key: '{}'",
253 ExtensionIdentifier, Key
254 );
255
256 #[cfg(feature = "AirIntegration")]
257 {
258 if let Some(AirClient) = &self.AirClient {
259 if IsAirAvailable(AirClient).await {
260 info!("[SecretProvider] Delegating GetSecret to Air service for key: '{}'", Key);
261
262 return GetSecretFromAir(AirClient, ExtensionIdentifier.clone(), Key).await;
263 } else {
264 warn!(
265 "[SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
266 Key
267 );
268 }
269 }
270 }
271
272 info!("[SecretProvider] Using local keyring for ext: '{}'", ExtensionIdentifier);
273
274 let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
275
276 let Entry = Entry::new(&ServiceName, &Key)
277 .map_err(|Error| CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() })?;
278
279 match Entry.get_password() {
280 Ok(Password) => Ok(Some(Password)),
281
282 Err(keyring::Error::NoEntry) => Ok(None),
283
284 Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
285 }
286 }
287
288 /// Stores a secret by writing to the OS keychain.
289 /// If Air is available and healthy, delegates to Air service.
290 /// Falls back to local keyring if Air is unavailable.
291 #[allow(unused_mut, unused_variables)]
292 async fn StoreSecret(&self, ExtensionIdentifier:String, Key:String, Value:String) -> Result<(), CommonError> {
293 info!(
294 "[SecretProvider] Storing secret for ext: '{}', key: '{}'",
295 ExtensionIdentifier, Key
296 );
297
298 #[cfg(feature = "AirIntegration")]
299 {
300 if let Some(AirClient) = &self.AirClient {
301 if IsAirAvailable(AirClient).await {
302 info!("[SecretProvider] Delegating StoreSecret to Air service for key: '{}'", Key);
303
304 return StoreSecretToAir(AirClient, ExtensionIdentifier.clone(), Key, Value).await;
305 } else {
306 warn!(
307 "[SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
308 Key
309 );
310 }
311 }
312 }
313
314 info!("[SecretProvider] Using local keyring for ext: '{}'", ExtensionIdentifier);
315
316 let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
317
318 let Entry = Entry::new(&ServiceName, &Key)
319 .map_err(|Error| CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() })?;
320
321 Entry
322 .set_password(&Value)
323 .map_err(|Error| CommonError::SecretsAccess { Key, Reason:Error.to_string() })
324 }
325
326 /// Deletes a secret by removing it from the OS keychain.
327 /// If Air is available and healthy, delegates to Air service.
328 /// Falls back to local keyring if Air is unavailable.
329 #[allow(unused_mut, unused_variables)]
330 async fn DeleteSecret(&self, ExtensionIdentifier:String, Key:String) -> Result<(), CommonError> {
331 info!(
332 "[SecretProvider] Deleting secret for ext: '{}', key: '{}'",
333 ExtensionIdentifier, Key
334 );
335
336 #[cfg(feature = "AirIntegration")]
337 {
338 if let Some(AirClient) = &self.AirClient {
339 if IsAirAvailable(AirClient).await {
340 info!("[SecretProvider] Delegating DeleteSecret to Air service for key: '{}'", Key);
341
342 return DeleteSecretFromAir(AirClient, ExtensionIdentifier.clone(), Key).await;
343 } else {
344 warn!(
345 "[SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
346 Key
347 );
348 }
349 }
350 }
351
352 info!("[SecretProvider] Using local keyring for ext: '{}'", ExtensionIdentifier);
353
354 let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
355
356 let Entry = Entry::new(&ServiceName, &Key)
357 .map_err(|Error| CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() })?;
358
359 match Entry.delete_credential() {
360 Ok(_) | Err(keyring::Error::NoEntry) => Ok(()),
361
362 Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
363 }
364 }
365}
366
367// ============================================================================
368// Air Integration Functions
369// ============================================================================
370
371#[cfg(feature = "AirIntegration")]
372use tonic::Request;
373
374/// Retrieves a secret from the Air service.
375#[cfg(feature = "AirIntegration")]
376async fn GetSecretFromAir(
377 AirClient:&AirServiceClient<tonic::transport::Channel>,
378 ExtensionIdentifier:String,
379 Key:String,
380) -> Result<Option<String>, CommonError> {
381 use AirLibrary::Vine::Generated::air::air_service_server;
382
383 info!(
384 "[SecretProvider] Fetching secret from Air: ext='{}', key='{}'",
385 ExtensionIdentifier, Key
386 );
387
388 // TODO: Implement Air secret retrieval by calling the Air service's GetSecret
389 // RPC method. This should:
390 // - Construct a GetSecretRequest with ExtensionIdentifier and Key
391 // - Call AirClient.get_secret (or similar) with appropriate timeout
392 // - Map Air service errors to CommonError (NotFound, AccessDenied, etc.)
393 // - Return Ok(Some(secret)) if found, Ok(None) if not found
394 // The Air service provides centralized secret storage with audit logging,
395 // access control, and cross-device sync capabilities.
396 Err(CommonError::NotImplemented { FeatureName:"GetSecretFromAir".to_string() })
397}
398
399/// Stores a secret in the Air service.
400#[cfg(feature = "AirIntegration")]
401async fn StoreSecretToAir(
402 AirClient:&AirServiceClient<tonic::transport::Channel>,
403 ExtensionIdentifier:String,
404 Key:String,
405 Value:String,
406) -> Result<(), CommonError> {
407 info!(
408 "[SecretProvider] Storing secret in Air: ext='{}', key='{}'",
409 ExtensionIdentifier, Key
410 );
411
412 // TODO: Implement Air secret storage by calling the Air service's StoreSecret
413 // RPC method. This should:
414 // - Construct a StoreSecretRequest with ExtensionIdentifier, Key, and Value
415 // - Call AirClient.store_secret (or similar) with the secret payload
416 // - Handle encryption and secure transmission to the Air service
417 // - Return Ok(()) on success, map errors to CommonError appropriately
418 // The Air service handles secret encryption at rest and provides fine-grained
419 // access control and versioning for secret updates.
420 Err(CommonError::NotImplemented { FeatureName:"StoreSecretToAir".to_string() })
421}
422
423/// Deletes a secret from the Air service.
424#[cfg(feature = "AirIntegration")]
425async fn DeleteSecretFromAir(
426 AirClient:&AirServiceClient<tonic::transport::Channel>,
427 ExtensionIdentifier:String,
428 Key:String,
429) -> Result<(), CommonError> {
430 info!(
431 "[SecretProvider] Deleting secret from Air: ext='{}', key='{}'",
432 ExtensionIdentifier, Key
433 );
434
435 // TODO: Implement Air secret deletion by calling the Air service's DeleteSecret
436 // RPC method. This should:
437 // - Construct a DeleteSecretRequest with ExtensionIdentifier and Key
438 // - Call AirClient.delete_secret (or similar) to remove the secret
439 // - Handle idempotency: deleting a non-existent secret should succeed
440 // - Return Ok(()) on success, map errors to CommonError as needed
441 // The Air service ensures secure deletion and propagates changes to other
442 // devices via sync, maintaining consistency across the user's ecosystem.
443 Err(CommonError::NotImplemented { FeatureName:"DeleteSecretFromAir".to_string() })
444}