Skip to content

Commit 2b3246b

Browse files
feat(Handler/secrets): Enhance secret storage handling with structured errors and improved reliability
- Integrated shared error utilities from `error_utils` for consistent RPC error formatting - Added detailed error mapping for `keyring` operations with extension-specific error codes (e.g., ESECRET_NOENTRY, ESECRET_PLATFORM) - Implemented trace/info/warn logging throughout lifecycle operations for better debugging - Updated service name generation to use Tauri bundle ID instead of package name for OS keychain stability - Added empty value warnings and duplicate entry handling for platform compatibility - Refactored parameter validation to use centralized error helpers with precise type checking These changes improve extension compatibility by aligning with VS Code's SecretStorage API error contracts while maintaining Land's security requirements. The enhanced logging supports Mountain's observability goals, and proper bundle ID usage prevents cross-app secret collisions.
1 parent 2ad1032 commit 2b3246b

2 files changed

Lines changed: 205 additions & 70 deletions

File tree

Source/Library.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ pub mod handlers {
3434
pub mod proxy;
3535

3636
pub mod registry;
37+
38+
pub mod secrets;
3739
}

Source/handlers/secrets.rs

Lines changed: 203 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,125 +2,258 @@
22
// Mountain Secrets Handlers (handlers/secrets.rs)
33
// --------------------------------------------------------------------------------------------
44
// Provides the backend implementation for the `vscode.SecretStorage` API,
5+
56
// handling secure storage and retrieval of sensitive extension data using the
67
// operating system's native keychain or credential store.
78
//
89
// Responsibilities:
910
// - Handling `$getPassword`, `$setPassword`, `$deletePassword` RPC calls
10-
// proxied from Cocoon's `secret-state-shim.js`.
11-
// - Interacting with the `keyring` crate to securely store, retrieve, and
12-
// delete secrets.
13-
// - Constructing appropriate service/username keys for the `keyring` crate,
14-
// typically based on the application identifier and the extension ID, to
15-
// ensure isolation.
16-
// - Mapping `keyring::Error` types to structured error messages/codes suitable
17-
// for returning to the Cocoon shim.
11+
// proxied from Cocoon's `secret-state-shim.js` (via Track/effects).
12+
// - Interacting with the `keyring` crate.
13+
// - Constructing service/username keys for `keyring`.
14+
// - Mapping `keyring::Error` to structured error messages/codes.
1815
//
1916
// Key Interactions:
20-
// - Called by `track::dispatch_sidecar_request` (or secrets effects) for RPC
21-
// methods.
22-
// - Uses the `keyring` crate for OS credential store interaction.
23-
// - Needs `AppHandle` to construct unique service names based on the app's
24-
// bundle ID.
17+
// - Called by effects created in `track.rs` or directly by RPC dispatcher.
18+
// - Uses the `keyring` crate.
19+
// - Needs `AppHandle` for app's bundle ID to create unique service names.
2520
// --------------------------------------------------------------------------------------------
2621

27-
// ----- START: Element/Mountain/src/handlers/secrets.rs -----
28-
use keyring::Entry; // Import the Entry type
22+
// Import the Entry type
23+
use keyring::Entry;
24+
// Added trace
25+
use log::{error, info, trace, warn};
2926
use serde_json::{Value, json};
30-
use tauri::{AppHandle, Runtime};
27+
// Added Manager for app.config()
28+
use tauri::{AppHandle, Manager, Runtime};
3129

32-
// Helper to create structured error string
33-
fn create_error_string(message:String, code:Option<&str>) -> String {
34-
json!({"message": message, "code": code.unwrap_or("EUNKNOWN")}).to_string()
35-
}
30+
// Use shared error utilities
31+
use crate::handlers::error_utils;
32+
33+
// Map keyring errors to a structured error string using shared utilities
34+
fn map_keyring_error_to_rpc_str(e:keyring::Error, operation:&str, key_context:&str) -> String {
35+
let error_message_prefix = format!("Keyring operation '{}' for key context '{}' failed", operation, key_context);
36+
37+
// Log the original keyring error with context
38+
error!("{}: {}", error_message_prefix, e);
39+
40+
let (specific_message, code_str) = match e.kind() {
41+
keyring::ErrorKind::NoEntry => (format!("{}: Secret not found.", error_message_prefix), "ESECRET_NOENTRY"),
42+
43+
keyring::ErrorKind::Ambiguous => {
44+
(
45+
format!(
46+
"{}: Ambiguous result, multiple entries found (unexpected).",
47+
error_message_prefix
48+
),
49+
"ESECRET_AMBIGUOUS",
50+
)
51+
},
52+
53+
keyring::ErrorKind::BadEncoding(_) => {
54+
(
55+
format!("{}: Data encoding/decoding error.", error_message_prefix),
56+
"ESECRET_ENCODING",
57+
)
58+
},
59+
60+
keyring::ErrorKind::InvalidAppId => {
61+
(
62+
format!(
63+
"{}: Invalid application identifier configuration for keyring.",
64+
error_message_prefix
65+
),
66+
"ESECRET_APPID",
67+
)
68+
},
69+
70+
keyring::ErrorKind::InvalidServiceName(_) => {
71+
(
72+
format!("{}: Invalid service name configuration for keyring.", error_message_prefix),
73+
"ESECRET_SERVICENAME",
74+
)
75+
},
76+
77+
keyring::ErrorKind::PlatformFailure(_) => {
78+
(
79+
format!(
80+
"{}: Underlying OS platform failure for keyring operation.",
81+
error_message_prefix
82+
),
83+
"ESECRET_PLATFORM",
84+
)
85+
},
3686

37-
// Map keyring errors to error strings
38-
fn map_keyring_error(e:keyring::Error, operation:&str) -> String {
39-
let code = match e {
40-
keyring::Error::NoEntry => "ENOENT",
41-
keyring::Error::Ambiguous => "EAMBIGUOUS", // Example custom code
42-
keyring::Error::BadEncoding(_) => "EBADENCODING",
43-
keyring::Error::InvalidAppId | keyring::Error::InvalidServiceName(_) => "EINVALIDAPPID",
44-
keyring::Error::PlatformFailure(_) => "EPLATFORM",
45-
keyring::Error::NoBackend => "ENOBACKEND",
46-
keyring::Error::BadPassword => "EBADPASS", // Maybe permission related?
47-
_ => "EKEYRING", // Generic
87+
keyring::ErrorKind::NoBackend => {
88+
(
89+
format!("{}: No suitable OS keychain/credential backend found.", error_message_prefix),
90+
"ESECRET_NOBACKEND",
91+
)
92+
},
93+
94+
keyring::ErrorKind::BadPassword => {
95+
(
96+
format!("{}: Incorrect password or permission issue with keyring.", error_message_prefix),
97+
"ESECRET_PERM",
98+
)
99+
},
100+
101+
keyring::ErrorKind::Duplicate => {
102+
(
103+
format!("{}: Attempted to create a duplicate secret entry.", error_message_prefix),
104+
"ESECRET_DUP",
105+
)
106+
},
107+
108+
keyring::ErrorKind::Cancelled => {
109+
(
110+
format!("{}: Keyring operation cancelled by user or system.", error_message_prefix),
111+
"ESECRET_CANCELLED",
112+
)
113+
},
114+
115+
// Catch-all for other kinds
116+
_ => {
117+
(
118+
format!("{}: Unknown keyring error occurred: {}", error_message_prefix, e),
119+
"ESECRET_UNKNOWN",
120+
)
121+
},
48122
};
49-
create_error_string(format!("Keyring {} error: {}", operation, e), Some(code))
123+
124+
error_utils::rpc_error_string(specific_message, Some(code_str))
50125
}
51126

52127
// Get service name for keyring entry (must be consistent)
53128
fn get_keyring_service_name<R:Runtime>(app:&AppHandle<R>, extension_id:&str) -> String {
54-
// Use App bundle identifier + extension ID for uniqueness
55-
// Make sure package_info() is available and correct.
56-
let app_name = app.package_info().name.clone();
57-
format!("{}.{}", app_name, extension_id)
129+
// Use App bundle identifier + extension ID for uniqueness and stability
130+
let app_bundle_id = app.config().tauri.bundle.identifier.clone();
131+
132+
format!("{}.{}", app_bundle_id, extension_id)
58133
}
59134

60135
pub async fn handle_get_secret<R:Runtime>(app:AppHandle<R>, params:Value) -> Result<Value, String> {
61136
let extension_id = params
62137
.get("extensionId")
63-
.and_then(|v| v.as_str())
64-
.ok_or_else(|| create_error_string("Missing extensionId".to_string(), Some("EBADARG")))?;
138+
.and_then(Value::as_str)
139+
.ok_or_else(|| error_utils::rpc_param_error_string("get_secret", "extensionId", "string", None))?;
140+
65141
let key = params
66142
.get("key")
67-
.and_then(|v| v.as_str())
68-
.ok_or_else(|| create_error_string("Missing key".to_string(), Some("EBADARG")))?;
69-
println!("[Secrets Handler] GetSecret ext={}, key={}", extension_id, key);
143+
.and_then(Value::as_str)
144+
.ok_or_else(|| error_utils::rpc_param_error_string("get_secret", "key", "string", None))?;
145+
146+
// Use trace for potentially frequent calls
147+
trace!("[Secrets Handler] GetSecret ext='{}', key='{}'", extension_id, key);
148+
149+
let service_name = get_keyring_service_name(&app, extension_id);
150+
151+
let key_context_for_error = format!("ext: '{}', key: '{}'", extension_id, key);
70152

71-
let service = get_keyring_service_name(&app, extension_id);
72-
let entry = Entry::new(&service, key); // Use key as username/account
153+
let entry = Entry::new(&service_name, key)
154+
.map_err(|e| map_keyring_error_to_rpc_str(e, "entry creation (get)", &key_context_for_error))?;
73155

74156
match entry.get_password() {
75-
Ok(password) => Ok(json!(password)), // Return password string as JSON
76-
Err(keyring::Error::NoEntry) => Ok(Value::Null), // Key not found, return null
77-
Err(e) => Err(map_keyring_error(e, "get")),
157+
Ok(password) => {
158+
trace!("[Secrets Handler] Secret found for ext='{}', key='{}'", extension_id, key);
159+
160+
// Return password string as JSON
161+
Ok(json!(password))
162+
},
163+
164+
Err(keyring::Error { kind: keyring::ErrorKind::NoEntry, .. }) => {
165+
trace!("[Secrets Handler] Secret not found for ext='{}', key='{}'", extension_id, key);
166+
167+
// Key not found is not an error for get, return null
168+
Ok(Value::Null)
169+
},
170+
171+
Err(e) => Err(map_keyring_error_to_rpc_str(e, "get_password", &key_context_for_error)),
78172
}
79173
}
80174

81175
pub async fn handle_store_secret<R:Runtime>(app:AppHandle<R>, params:Value) -> Result<Value, String> {
82176
let extension_id = params
83177
.get("extensionId")
84-
.and_then(|v| v.as_str())
85-
.ok_or_else(|| create_error_string("Missing extensionId".to_string(), Some("EBADARG")))?;
178+
.and_then(Value::as_str)
179+
.ok_or_else(|| error_utils::rpc_param_error_string("store_secret", "extensionId", "string", None))?;
180+
86181
let key = params
87182
.get("key")
88-
.and_then(|v| v.as_str())
89-
.ok_or_else(|| create_error_string("Missing key".to_string(), Some("EBADARG")))?;
183+
.and_then(Value::as_str)
184+
.ok_or_else(|| error_utils::rpc_param_error_string("store_secret", "key", "string", None))?;
185+
90186
// Value MUST be a string for keyring
91187
let value = params
92188
.get("value")
93-
.and_then(|v| v.as_str())
94-
.ok_or_else(|| create_error_string("Missing or invalid value (must be string)".to_string(), Some("EBADARG")))?;
95-
println!("[Secrets Handler] StoreSecret ext={}, key={}", extension_id, key);
189+
.and_then(Value::as_str)
190+
.ok_or_else(|| error_utils::rpc_param_error_string("store_secret", "value", "string", None))?;
191+
192+
if value.is_empty() {
193+
warn!(
194+
"[Secrets Handler] Storing empty string for secret ext='{}', key='{}'. This might behave unexpectedly on \
195+
some OS keyring backends.",
196+
extension_id, key
197+
);
198+
}
199+
200+
// Info as it's a modification
201+
info!("[Secrets Handler] StoreSecret ext='{}', key='{}'", extension_id, key);
202+
203+
let service_name = get_keyring_service_name(&app, extension_id);
96204

97-
let service = get_keyring_service_name(&app, extension_id);
98-
let entry = Entry::new(&service, key);
205+
let key_context_for_error = format!("ext: '{}', key: '{}'", extension_id, key);
99206

100-
entry
101-
.set_password(value)
102-
.map(|_| Value::Null) // Return JSON null on success
103-
.map_err(|e| map_keyring_error(e, "store"))
207+
let entry = Entry::new(&service_name, key)
208+
.map_err(|e| map_keyring_error_to_rpc_str(e, "entry creation (store)", &key_context_for_error))?;
209+
210+
entry.set_password(value)
211+
// Return JSON null on success
212+
.map(|_| Value::Null)
213+
.map_err(|e| map_keyring_error_to_rpc_str(e, "set_password", &key_context_for_error))
104214
}
105215

106216
pub async fn handle_delete_secret<R:Runtime>(app:AppHandle<R>, params:Value) -> Result<Value, String> {
107217
let extension_id = params
108218
.get("extensionId")
109-
.and_then(|v| v.as_str())
110-
.ok_or_else(|| create_error_string("Missing extensionId".to_string(), Some("EBADARG")))?;
219+
.and_then(Value::as_str)
220+
.ok_or_else(|| error_utils::rpc_param_error_string("delete_secret", "extensionId", "string", None))?;
221+
111222
let key = params
112223
.get("key")
113-
.and_then(|v| v.as_str())
114-
.ok_or_else(|| create_error_string("Missing key".to_string(), Some("EBADARG")))?;
115-
println!("[Secrets Handler] DeleteSecret ext={}, key={}", extension_id, key);
224+
.and_then(Value::as_str)
225+
.ok_or_else(|| error_utils::rpc_param_error_string("delete_secret", "key", "string", None))?;
226+
227+
// Info as it's a modification
228+
info!("[Secrets Handler] DeleteSecret ext='{}', key='{}'", extension_id, key);
229+
230+
let service_name = get_keyring_service_name(&app, extension_id);
231+
232+
let key_context_for_error = format!("ext: '{}', key: '{}'", extension_id, key);
116233

117-
let service = get_keyring_service_name(&app, extension_id);
118-
let entry = Entry::new(&service, key);
234+
let entry = Entry::new(&service_name, key)
235+
.map_err(|e| map_keyring_error_to_rpc_str(e, "entry creation (delete)", &key_context_for_error))?;
119236

120237
match entry.delete_password() {
121-
Ok(_) => Ok(Value::Null),
122-
Err(keyring::Error::NoEntry) => Ok(Value::Null), // OK if not found
123-
Err(e) => Err(map_keyring_error(e, "delete")),
238+
Ok(_) => {
239+
info!(
240+
"[Secrets Handler] Secret deleted successfully for ext='{}', key='{}'",
241+
extension_id, key
242+
);
243+
244+
Ok(Value::Null)
245+
},
246+
247+
Err(keyring::Error { kind: keyring::ErrorKind::NoEntry, .. }) => {
248+
info!(
249+
"[Secrets Handler] Secret not found for deletion (ext='{}', key='{}'), considered success.",
250+
extension_id, key
251+
);
252+
253+
// OK if not found
254+
Ok(Value::Null)
255+
},
256+
257+
Err(e) => Err(map_keyring_error_to_rpc_str(e, "delete_password", &key_context_for_error)),
124258
}
125259
}
126-
// ----- END: Element/Mountain/src/handlers/secrets.rs -----

0 commit comments

Comments
 (0)