Skip to main content

Mountain/Environment/
SecretProvider.rs

1//! # SecretProvider (Environment)
2//!
3//! Implements the `SecretProvider` trait for `MountainEnvironment`. Contains
4//! the core logic for secure secret storage using the system keyring, powered
5//! by the `keyring` crate.
6//!
7//! ## Keyring integration
8//!
9//! The `keyring` crate provides cross-platform secure storage:
10//! - **macOS**: Native Keychain (OSXKeychain)
11//! - **Windows**: Windows Credential Manager (WinCredential)
12//! - **Linux**: Secret Service API (dbus-secret-service) or GNOME Keyring
13//!
14//! Each secret is identified by a service name
15//! (`<app>.<ExtensionIdentifier>`) and a key string.
16//!
17//! ## Security considerations
18//!
19//! 1. Secrets are never logged or included in error messages.
20//! 2. The keyring handles encryption at the OS level.
21//! 3. OS keychain manages access permissions and unlocking.
22//! 4. Failed operations do not expose secret values.
23//! 5. Extension and key identifiers are validated before use.
24//!
25//! ## Air integration
26//!
27//! When the `AirIntegration` feature is enabled, `GetSecret`, `StoreSecret`,
28//! and `DeleteSecret` delegate to Air service RPCs when the client is healthy,
29//! falling back to the local keyring otherwise. The three Air stub functions
30//! (`GetSecretFromAir`, `StoreSecretToAir`, `DeleteSecretFromAir`) are gated
31//! behind `#[cfg(feature = "AirIntegration")]` and currently return
32//! `NotImplemented`.
33//!
34//! ## VS Code reference
35//!
36//! - `vs/platform/secrets/common/secrets.ts`
37//! - `vs/platform/secrets/electron-simulator/electronSecretStorage.ts`
38
39use CommonLibrary::{Error::CommonError::CommonError, Secret::SecretProvider::SecretProvider};
40use async_trait::async_trait;
41use keyring_core::{Entry, Error as KeyringError};
42// Import Air client types when Air is available in the workspace
43#[cfg(feature = "AirIntegration")]
44use AirLibrary::Vine::Generated::air::air_service_client::AirServiceClient;
45
46use super::MountainEnvironment::MountainEnvironment;
47use crate::dev_log;
48
49/// Constructs the service name for the keyring entry.
50fn GetKeyringServiceName(Environment:&MountainEnvironment, ExtensionIdentifier:&str) -> String {
51	format!("{}.{}", Environment.ApplicationHandle.package_info().name, ExtensionIdentifier)
52}
53
54/// Helper to check if the Air gRPC client is available without a
55/// proper health check. The raw client requires `&mut self` for
56/// `health_check`, but `MountainEnvironment` holds an immutable
57/// reference. This returns `true` whenever a client is attached.
58/// Blocked on proper wrapper integration.
59#[cfg(feature = "AirIntegration")]
60async fn IsAirAvailable(_AirClient:&AirServiceClient<tonic::transport::Channel>) -> bool {
61	// TODO: implement proper health check when AirClient wrapper supports
62	// &mut self for health_check RPC. MountainEnvironment stores an
63	// immutable reference, so this is blocked on wrapper integration.
64	true
65}
66
67#[async_trait]
68impl SecretProvider for MountainEnvironment {
69	/// Retrieves a secret by reading from the OS keychain.
70	///
71	/// When `AirIntegration` is enabled, attempts to delegate to the Air
72	/// service first and falls back to the local keyring on failure.
73	/// Returns `Ok(None)` if the keychain entry does not exist.
74	#[cfg_attr(not(feature = "AirIntegration"), allow(unused_mut))]
75	async fn GetSecret(&self, ExtensionIdentifier:String, Key:String) -> Result<Option<String>, CommonError> {
76		dev_log!(
77			"storage-verbose",
78			"[SecretProvider] Getting secret for ext: '{}', key: '{}'",
79			ExtensionIdentifier,
80			Key
81		);
82
83		#[cfg(feature = "AirIntegration")]
84		{
85			if let Some(AirClient) = &self.AirClient {
86				if IsAirAvailable(AirClient).await {
87					dev_log!(
88						"storage-verbose",
89						"[SecretProvider] Delegating GetSecret to Air service for key: '{}'",
90						Key
91					);
92
93					return GetSecretFromAir(AirClient, ExtensionIdentifier.clone(), Key).await;
94				} else {
95					dev_log!(
96						"storage",
97						"warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
98						Key
99					);
100				}
101			}
102		}
103
104		dev_log!(
105			"storage-verbose",
106			"[SecretProvider] Using local keyring for ext: '{}'",
107			ExtensionIdentifier
108		);
109
110		let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
111
112		let Entry = match Entry::new(&ServiceName, &Key) {
113			Ok(e) => e,
114			Err(KeyringError::NoStorageAccess(_)) | Err(KeyringError::PlatformFailure(_)) => {
115				dev_log!(
116					"storage",
117					"warn: [SecretProvider] Keyring unavailable for key '{}', returning None",
118					Key
119				);
120				return Ok(None);
121			},
122			Err(Error) => return Err(CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() }),
123		};
124
125		match Entry.get_password() {
126			Ok(Password) => Ok(Some(Password)),
127
128			Err(KeyringError::NoEntry) => Ok(None),
129
130			Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
131		}
132	}
133
134	/// Stores a secret by writing to the OS keychain.
135	///
136	/// When `AirIntegration` is enabled, attempts to delegate to the Air
137	/// service first and falls back to the local keyring on failure.
138	#[cfg_attr(not(feature = "AirIntegration"), allow(unused_mut))]
139	async fn StoreSecret(&self, ExtensionIdentifier:String, Key:String, Value:String) -> Result<(), CommonError> {
140		dev_log!(
141			"storage-verbose",
142			"[SecretProvider] Storing secret for ext: '{}', key: '{}'",
143			ExtensionIdentifier,
144			Key
145		);
146
147		#[cfg(feature = "AirIntegration")]
148		{
149			if let Some(AirClient) = &self.AirClient {
150				if IsAirAvailable(AirClient).await {
151					dev_log!(
152						"storage-verbose",
153						"[SecretProvider] Delegating StoreSecret to Air service for key: '{}'",
154						Key
155					);
156
157					return StoreSecretToAir(AirClient, ExtensionIdentifier.clone(), Key, Value).await;
158				} else {
159					dev_log!(
160						"storage",
161						"warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
162						Key
163					);
164				}
165			}
166		}
167
168		dev_log!(
169			"storage-verbose",
170			"[SecretProvider] Using local keyring for ext: '{}'",
171			ExtensionIdentifier
172		);
173
174		let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
175
176		let Entry = match Entry::new(&ServiceName, &Key) {
177			Ok(e) => e,
178			Err(KeyringError::NoStorageAccess(_)) | Err(KeyringError::PlatformFailure(_)) => {
179				dev_log!(
180					"storage",
181					"warn: [SecretProvider] Keyring unavailable for key '{}', cannot store",
182					Key
183				);
184				return Ok(());
185			},
186			Err(Error) => return Err(CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() }),
187		};
188
189		Entry
190			.set_password(&Value)
191			.map_err(|Error| CommonError::SecretsAccess { Key, Reason:Error.to_string() })
192	}
193
194	/// Deletes a secret by removing it from the OS keychain.
195	///
196	/// When `AirIntegration` is enabled, attempts to delegate to the Air
197	/// service first and falls back to the local keyring on failure.
198	/// Idempotent: removing a non-existent entry is treated as success.
199	#[cfg_attr(not(feature = "AirIntegration"), allow(unused_mut))]
200	async fn DeleteSecret(&self, ExtensionIdentifier:String, Key:String) -> Result<(), CommonError> {
201		dev_log!(
202			"storage-verbose",
203			"[SecretProvider] Deleting secret for ext: '{}', key: '{}'",
204			ExtensionIdentifier,
205			Key
206		);
207
208		#[cfg(feature = "AirIntegration")]
209		{
210			if let Some(AirClient) = &self.AirClient {
211				if IsAirAvailable(AirClient).await {
212					dev_log!(
213						"storage-verbose",
214						"[SecretProvider] Delegating DeleteSecret to Air service for key: '{}'",
215						Key
216					);
217
218					return DeleteSecretFromAir(AirClient, ExtensionIdentifier.clone(), Key).await;
219				} else {
220					dev_log!(
221						"storage",
222						"warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
223						Key
224					);
225				}
226			}
227		}
228
229		dev_log!(
230			"storage-verbose",
231			"[SecretProvider] Using local keyring for ext: '{}'",
232			ExtensionIdentifier
233		);
234
235		let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
236
237		let Entry = match Entry::new(&ServiceName, &Key) {
238			Ok(e) => e,
239			Err(KeyringError::NoStorageAccess(_)) | Err(KeyringError::PlatformFailure(_)) => {
240				dev_log!(
241					"storage",
242					"warn: [SecretProvider] Keyring unavailable for key '{}', cannot delete",
243					Key
244				);
245				return Ok(());
246			},
247			Err(Error) => return Err(CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() }),
248		};
249
250		match Entry.delete_credential() {
251			Ok(_) | Err(KeyringError::NoEntry) => Ok(()),
252
253			Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
254		}
255	}
256}
257
258// ============================================================================
259// Air Integration Functions
260// ============================================================================
261
262/// Air stub: retrieves a secret from the remote Air service.
263///
264/// TODO: construct GetSecretRequest with ExtensionIdentifier + Key, call
265/// AirClient.get_secret with timeout, map errors to CommonError, return
266/// Ok(Some(secret)) if found or Ok(None) if not found.
267#[cfg(feature = "AirIntegration")]
268async fn GetSecretFromAir(
269	_AirClient:&AirServiceClient<tonic::transport::Channel>,
270
271	ExtensionIdentifier:String,
272
273	Key:String,
274) -> Result<Option<String>, CommonError> {
275	dev_log!(
276		"storage",
277		"[SecretProvider] Fetching secret from Air: ext='{}', key='{}'",
278		ExtensionIdentifier,
279		Key
280	);
281
282	// TODO: construct GetSecretRequest with ExtensionIdentifier + Key, call
283	// AirClient.get_secret with timeout, map errors to CommonError, return
284	// Ok(Some(secret)) if found / Ok(None) if not found.
285	Err(CommonError::NotImplemented { FeatureName:"GetSecretFromAir".to_string() })
286}
287
288/// Air stub: stores a secret in the remote Air service.
289///
290/// TODO: construct StoreSecretRequest with ExtensionIdentifier, Key, Value;
291/// handle encryption and secure transmission; map errors to CommonError.
292#[cfg(feature = "AirIntegration")]
293async fn StoreSecretToAir(
294	_AirClient:&AirServiceClient<tonic::transport::Channel>,
295
296	ExtensionIdentifier:String,
297
298	Key:String,
299
300	_Value:String,
301) -> Result<(), CommonError> {
302	dev_log!(
303		"storage",
304		"[SecretProvider] Storing secret in Air: ext='{}', key='{}'",
305		ExtensionIdentifier,
306		Key
307	);
308
309	// TODO: construct StoreSecretRequest with ExtensionIdentifier, Key, Value;
310	// handle encryption and secure transmission; map errors to CommonError.
311	Err(CommonError::NotImplemented { FeatureName:"StoreSecretToAir".to_string() })
312}
313
314/// Deletes a secret from the Air service.
315#[cfg(feature = "AirIntegration")]
316async fn DeleteSecretFromAir(
317	_AirClient:&AirServiceClient<tonic::transport::Channel>,
318
319	ExtensionIdentifier:String,
320
321	Key:String,
322) -> Result<(), CommonError> {
323	dev_log!(
324		"storage",
325		"[SecretProvider] Deleting secret from Air: ext='{}', key='{}'",
326		ExtensionIdentifier,
327		Key
328	);
329
330	// TODO: construct DeleteSecretRequest, handle idempotency (missing secret
331	// is success), map errors to CommonError.
332	Err(CommonError::NotImplemented { FeatureName:"DeleteSecretFromAir".to_string() })
333}