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
115			Err(KeyringError::NoStorageAccess(_)) | Err(KeyringError::PlatformFailure(_)) => {
116				dev_log!(
117					"storage",
118					"warn: [SecretProvider] Keyring unavailable for key '{}', returning None",
119					Key
120				);
121
122				return Ok(None);
123			},
124
125			Err(Error) => return Err(CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() }),
126		};
127
128		match Entry.get_password() {
129			Ok(Password) => Ok(Some(Password)),
130
131			Err(KeyringError::NoEntry) => Ok(None),
132
133			Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
134		}
135	}
136
137	/// Stores a secret by writing to the OS keychain.
138	///
139	/// When `AirIntegration` is enabled, attempts to delegate to the Air
140	/// service first and falls back to the local keyring on failure.
141	#[cfg_attr(not(feature = "AirIntegration"), allow(unused_mut))]
142	async fn StoreSecret(&self, ExtensionIdentifier:String, Key:String, Value:String) -> Result<(), CommonError> {
143		dev_log!(
144			"storage-verbose",
145			"[SecretProvider] Storing secret for ext: '{}', key: '{}'",
146			ExtensionIdentifier,
147			Key
148		);
149
150		#[cfg(feature = "AirIntegration")]
151		{
152			if let Some(AirClient) = &self.AirClient {
153				if IsAirAvailable(AirClient).await {
154					dev_log!(
155						"storage-verbose",
156						"[SecretProvider] Delegating StoreSecret to Air service for key: '{}'",
157						Key
158					);
159
160					return StoreSecretToAir(AirClient, ExtensionIdentifier.clone(), Key, Value).await;
161				} else {
162					dev_log!(
163						"storage",
164						"warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
165						Key
166					);
167				}
168			}
169		}
170
171		dev_log!(
172			"storage-verbose",
173			"[SecretProvider] Using local keyring for ext: '{}'",
174			ExtensionIdentifier
175		);
176
177		let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
178
179		let Entry = match Entry::new(&ServiceName, &Key) {
180			Ok(e) => e,
181
182			Err(KeyringError::NoStorageAccess(_)) | Err(KeyringError::PlatformFailure(_)) => {
183				dev_log!(
184					"storage",
185					"warn: [SecretProvider] Keyring unavailable for key '{}', cannot store",
186					Key
187				);
188
189				return Ok(());
190			},
191
192			Err(Error) => return Err(CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() }),
193		};
194
195		Entry
196			.set_password(&Value)
197			.map_err(|Error| CommonError::SecretsAccess { Key, Reason:Error.to_string() })
198	}
199
200	/// Deletes a secret by removing it from the OS keychain.
201	///
202	/// When `AirIntegration` is enabled, attempts to delegate to the Air
203	/// service first and falls back to the local keyring on failure.
204	/// Idempotent: removing a non-existent entry is treated as success.
205	#[cfg_attr(not(feature = "AirIntegration"), allow(unused_mut))]
206	async fn DeleteSecret(&self, ExtensionIdentifier:String, Key:String) -> Result<(), CommonError> {
207		dev_log!(
208			"storage-verbose",
209			"[SecretProvider] Deleting secret for ext: '{}', key: '{}'",
210			ExtensionIdentifier,
211			Key
212		);
213
214		#[cfg(feature = "AirIntegration")]
215		{
216			if let Some(AirClient) = &self.AirClient {
217				if IsAirAvailable(AirClient).await {
218					dev_log!(
219						"storage-verbose",
220						"[SecretProvider] Delegating DeleteSecret to Air service for key: '{}'",
221						Key
222					);
223
224					return DeleteSecretFromAir(AirClient, ExtensionIdentifier.clone(), Key).await;
225				} else {
226					dev_log!(
227						"storage",
228						"warn: [SecretProvider] Air client unavailable, falling back to local keyring for key: '{}'",
229						Key
230					);
231				}
232			}
233		}
234
235		dev_log!(
236			"storage-verbose",
237			"[SecretProvider] Using local keyring for ext: '{}'",
238			ExtensionIdentifier
239		);
240
241		let ServiceName = GetKeyringServiceName(self, &ExtensionIdentifier);
242
243		let Entry = match Entry::new(&ServiceName, &Key) {
244			Ok(e) => e,
245
246			Err(KeyringError::NoStorageAccess(_)) | Err(KeyringError::PlatformFailure(_)) => {
247				dev_log!(
248					"storage",
249					"warn: [SecretProvider] Keyring unavailable for key '{}', cannot delete",
250					Key
251				);
252
253				return Ok(());
254			},
255
256			Err(Error) => return Err(CommonError::SecretsAccess { Key:Key.clone(), Reason:Error.to_string() }),
257		};
258
259		match Entry.delete_credential() {
260			Ok(_) | Err(KeyringError::NoEntry) => Ok(()),
261
262			Err(Error) => Err(CommonError::SecretsAccess { Key, Reason:Error.to_string() }),
263		}
264	}
265}
266
267// ============================================================================
268// Air Integration Functions
269// ============================================================================
270
271/// Air stub: retrieves a secret from the remote Air service.
272///
273/// TODO: construct GetSecretRequest with ExtensionIdentifier + Key, call
274/// AirClient.get_secret with timeout, map errors to CommonError, return
275/// Ok(Some(secret)) if found or Ok(None) if not found.
276#[cfg(feature = "AirIntegration")]
277async fn GetSecretFromAir(
278	_AirClient:&AirServiceClient<tonic::transport::Channel>,
279
280	ExtensionIdentifier:String,
281
282	Key:String,
283) -> Result<Option<String>, CommonError> {
284	dev_log!(
285		"storage",
286		"[SecretProvider] Fetching secret from Air: ext='{}', key='{}'",
287		ExtensionIdentifier,
288		Key
289	);
290
291	// TODO: construct GetSecretRequest with ExtensionIdentifier + Key, call
292	// AirClient.get_secret with timeout, map errors to CommonError, return
293	// Ok(Some(secret)) if found / Ok(None) if not found.
294	Err(CommonError::NotImplemented { FeatureName:"GetSecretFromAir".to_string() })
295}
296
297/// Air stub: stores a secret in the remote Air service.
298///
299/// TODO: construct StoreSecretRequest with ExtensionIdentifier, Key, Value;
300/// handle encryption and secure transmission; map errors to CommonError.
301#[cfg(feature = "AirIntegration")]
302async fn StoreSecretToAir(
303	_AirClient:&AirServiceClient<tonic::transport::Channel>,
304
305	ExtensionIdentifier:String,
306
307	Key:String,
308
309	_Value:String,
310) -> Result<(), CommonError> {
311	dev_log!(
312		"storage",
313		"[SecretProvider] Storing secret in Air: ext='{}', key='{}'",
314		ExtensionIdentifier,
315		Key
316	);
317
318	// TODO: construct StoreSecretRequest with ExtensionIdentifier, Key, Value;
319	// handle encryption and secure transmission; map errors to CommonError.
320	Err(CommonError::NotImplemented { FeatureName:"StoreSecretToAir".to_string() })
321}
322
323/// Deletes a secret from the Air service.
324#[cfg(feature = "AirIntegration")]
325async fn DeleteSecretFromAir(
326	_AirClient:&AirServiceClient<tonic::transport::Channel>,
327
328	ExtensionIdentifier:String,
329
330	Key:String,
331) -> Result<(), CommonError> {
332	dev_log!(
333		"storage",
334		"[SecretProvider] Deleting secret from Air: ext='{}', key='{}'",
335		ExtensionIdentifier,
336		Key
337	);
338
339	// TODO: construct DeleteSecretRequest, handle idempotency (missing secret
340	// is success), map errors to CommonError.
341	Err(CommonError::NotImplemented { FeatureName:"DeleteSecretFromAir".to_string() })
342}