Skip to main content

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}