|
2 | 2 | // Mountain Secrets Handlers (handlers/secrets.rs) |
3 | 3 | // -------------------------------------------------------------------------------------------- |
4 | 4 | // Provides the backend implementation for the `vscode.SecretStorage` API, |
| 5 | + |
5 | 6 | // handling secure storage and retrieval of sensitive extension data using the |
6 | 7 | // operating system's native keychain or credential store. |
7 | 8 | // |
8 | 9 | // Responsibilities: |
9 | 10 | // - 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. |
18 | 15 | // |
19 | 16 | // 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. |
25 | 20 | // -------------------------------------------------------------------------------------------- |
26 | 21 |
|
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}; |
29 | 26 | use serde_json::{Value, json}; |
30 | | -use tauri::{AppHandle, Runtime}; |
| 27 | +// Added Manager for app.config() |
| 28 | +use tauri::{AppHandle, Manager, Runtime}; |
31 | 29 |
|
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 | + }, |
36 | 86 |
|
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 | + }, |
48 | 122 | }; |
49 | | - create_error_string(format!("Keyring {} error: {}", operation, e), Some(code)) |
| 123 | + |
| 124 | + error_utils::rpc_error_string(specific_message, Some(code_str)) |
50 | 125 | } |
51 | 126 |
|
52 | 127 | // Get service name for keyring entry (must be consistent) |
53 | 128 | 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) |
58 | 133 | } |
59 | 134 |
|
60 | 135 | pub async fn handle_get_secret<R:Runtime>(app:AppHandle<R>, params:Value) -> Result<Value, String> { |
61 | 136 | let extension_id = params |
62 | 137 | .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 | + |
65 | 141 | let key = params |
66 | 142 | .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); |
70 | 152 |
|
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))?; |
73 | 155 |
|
74 | 156 | 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)), |
78 | 172 | } |
79 | 173 | } |
80 | 174 |
|
81 | 175 | pub async fn handle_store_secret<R:Runtime>(app:AppHandle<R>, params:Value) -> Result<Value, String> { |
82 | 176 | let extension_id = params |
83 | 177 | .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 | + |
86 | 181 | let key = params |
87 | 182 | .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 | + |
90 | 186 | // Value MUST be a string for keyring |
91 | 187 | let value = params |
92 | 188 | .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); |
96 | 204 |
|
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); |
99 | 206 |
|
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)) |
104 | 214 | } |
105 | 215 |
|
106 | 216 | pub async fn handle_delete_secret<R:Runtime>(app:AppHandle<R>, params:Value) -> Result<Value, String> { |
107 | 217 | let extension_id = params |
108 | 218 | .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 | + |
111 | 222 | let key = params |
112 | 223 | .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); |
116 | 233 |
|
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))?; |
119 | 236 |
|
120 | 237 | 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)), |
124 | 258 | } |
125 | 259 | } |
126 | | -// ----- END: Element/Mountain/src/handlers/secrets.rs ----- |
|
0 commit comments