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}