1use std::collections::HashMap;
2use std::fmt;
3use std::sync::{Arc, Mutex, OnceLock};
4
5use async_trait::async_trait;
6use keyring_core::{CredentialStore, Entry, Error as KeyringError};
7
8use super::{
9 emit_secret_access_event, ensure_scoped_secret_access_allowed, RotationHandle, SecretBytes,
10 SecretDeleteRequest, SecretError, SecretId, SecretMeta, SecretProvider,
11};
12
13static PLATFORM_STORE: OnceLock<Arc<CredentialStore>> = OnceLock::new();
14
15#[derive(Debug, thiserror::Error)]
16pub enum NativeKeyringError {
17 #[error(transparent)]
18 Keyring(#[from] KeyringError),
19 #[error("credential contains invalid UTF-8: {0}")]
20 Utf8(#[from] std::string::FromUtf8Error),
21}
22
23pub struct NativeKeyring {
29 service: String,
30 entries: Mutex<HashMap<String, Arc<Entry>>>,
31 store: Option<Arc<CredentialStore>>,
32}
33
34impl fmt::Debug for NativeKeyring {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter
37 .debug_struct("NativeKeyring")
38 .field("service", &self.service)
39 .finish_non_exhaustive()
40 }
41}
42
43impl NativeKeyring {
44 pub fn new(service: impl Into<String>) -> Self {
45 Self {
46 service: service.into(),
47 entries: Mutex::new(HashMap::new()),
48 store: None,
49 }
50 }
51
52 #[cfg(test)]
53 fn with_store(service: impl Into<String>, store: Arc<CredentialStore>) -> Self {
54 Self {
55 service: service.into(),
56 entries: Mutex::new(HashMap::new()),
57 store: Some(store),
58 }
59 }
60
61 pub fn service(&self) -> &str {
62 &self.service
63 }
64
65 pub fn get(&self, user: &str) -> Result<Option<Vec<u8>>, NativeKeyringError> {
66 match self.entry(user)?.get_secret() {
67 Ok(secret) => Ok(Some(secret)),
68 Err(KeyringError::NoEntry) => Ok(None),
69 Err(error) => Err(error.into()),
70 }
71 }
72
73 pub fn get_string(&self, user: &str) -> Result<Option<String>, NativeKeyringError> {
74 self.get(user)?
75 .map(String::from_utf8)
76 .transpose()
77 .map_err(Into::into)
78 }
79
80 pub fn set(&self, user: &str, secret: &[u8]) -> Result<(), NativeKeyringError> {
81 self.entry(user)?.set_secret(secret).map_err(Into::into)
82 }
83
84 pub fn set_string(&self, user: &str, secret: &str) -> Result<(), NativeKeyringError> {
85 self.set(user, secret.as_bytes())
86 }
87
88 pub fn delete(&self, user: &str) -> Result<bool, NativeKeyringError> {
89 match self.entry(user)?.delete_credential() {
90 Ok(()) => Ok(true),
91 Err(KeyringError::NoEntry) => Ok(false),
92 Err(error) => Err(error.into()),
93 }
94 }
95
96 pub fn list(&self) -> Result<Vec<String>, NativeKeyringError> {
97 let store = self.store()?;
98 #[cfg(target_os = "windows")]
99 let pattern = format!(r"\.{}$", regex::escape(&self.service));
100 #[cfg(target_os = "windows")]
101 let spec = HashMap::from([("pattern", pattern.as_str())]);
102 #[cfg(not(target_os = "windows"))]
103 let spec = HashMap::from([("service", self.service.as_str())]);
104 let mut users = store
105 .search(&spec)?
106 .into_iter()
107 .filter_map(|entry| entry.get_specifiers())
108 .filter_map(|(service, user)| (service == self.service).then_some(user))
109 .collect::<Vec<_>>();
110 users.sort();
111 users.dedup();
112 Ok(users)
113 }
114
115 pub fn healthcheck(&self) -> Result<String, NativeKeyringError> {
116 let _ = self.get("__harn_probe__")?;
117 Ok(format!("service '{}' reachable", self.service))
118 }
119
120 fn entry(&self, user: &str) -> Result<Arc<Entry>, NativeKeyringError> {
121 let mut entries = self.entries.lock().expect("keyring cache poisoned");
122 if let Some(entry) = entries.get(user) {
123 return Ok(entry.clone());
124 }
125 let entry = Arc::new(self.store()?.build(self.service(), user, None)?);
126 entries.insert(user.to_string(), entry.clone());
127 Ok(entry)
128 }
129
130 fn store(&self) -> Result<Arc<CredentialStore>, NativeKeyringError> {
131 if let Some(store) = &self.store {
132 return Ok(store.clone());
133 }
134 if let Some(store) = PLATFORM_STORE.get() {
135 return Ok(store.clone());
136 }
137 let store = platform_store()?;
138 let _ = PLATFORM_STORE.set(store.clone());
139 Ok(PLATFORM_STORE.get().cloned().unwrap_or(store))
140 }
141}
142
143fn platform_store() -> Result<Arc<CredentialStore>, NativeKeyringError> {
144 #[cfg(all(feature = "native-keyring", target_os = "macos"))]
145 {
146 let store: Arc<CredentialStore> = apple_native_keyring_store::keychain::Store::new()?;
147 return Ok(store);
148 }
149 #[cfg(all(feature = "native-keyring", target_os = "ios"))]
150 {
151 let store: Arc<CredentialStore> = apple_native_keyring_store::protected::Store::new()?;
152 return Ok(store);
153 }
154 #[cfg(all(feature = "native-keyring", target_os = "windows"))]
155 {
156 let store: Arc<CredentialStore> = windows_native_keyring_store::Store::new()?;
157 return Ok(store);
158 }
159 #[cfg(all(
160 feature = "native-keyring",
161 unix,
162 not(any(target_os = "macos", target_os = "ios", target_os = "android"))
163 ))]
164 {
165 let store: Arc<CredentialStore> = zbus_secret_service_keyring_store::Store::new()?;
166 return Ok(store);
167 }
168 #[allow(unreachable_code)]
169 Err(KeyringError::NoDefaultStore.into())
170}
171
172#[derive(Debug)]
173pub struct KeyringSecretProvider {
174 keyring: NativeKeyring,
175}
176
177impl KeyringSecretProvider {
178 pub fn new(namespace: impl Into<String>) -> Self {
179 Self {
180 keyring: NativeKeyring::new(namespace),
181 }
182 }
183
184 #[cfg(test)]
185 pub(super) fn with_store(namespace: impl Into<String>, store: Arc<CredentialStore>) -> Self {
186 Self {
187 keyring: NativeKeyring::with_store(namespace, store),
188 }
189 }
190
191 pub fn service(&self) -> &str {
192 self.keyring.service()
193 }
194
195 pub async fn delete(&self, id: &SecretId) -> Result<(), SecretError> {
196 self.keyring
197 .delete(&account_name(id))
198 .map(|_| ())
199 .map_err(|error| backend_error("delete", error))
200 }
201
202 pub fn healthcheck(&self) -> Result<String, SecretError> {
203 self.keyring
204 .healthcheck()
205 .map_err(|error| backend_error("access", error))
206 }
207}
208
209#[async_trait]
210impl SecretProvider for KeyringSecretProvider {
211 async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
212 match self
213 .keyring
214 .get(&account_name(id))
215 .map_err(|error| backend_error("read", error))?
216 {
217 Some(bytes) => {
218 emit_secret_access_event("keyring", id);
219 Ok(SecretBytes::from(bytes))
220 }
221 None => Err(SecretError::NotFound {
222 provider: "keyring".to_string(),
223 id: id.clone(),
224 }),
225 }
226 }
227
228 async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
229 value.with_exposed(|bytes| {
230 self.keyring
231 .set(&account_name(id), bytes)
232 .map_err(|error| backend_error("store", error))
233 })
234 }
235
236 async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
237 Err(SecretError::Unsupported {
238 provider: "keyring".to_string(),
239 operation: "rotate",
240 })
241 }
242
243 async fn delete_scoped(&self, request: SecretDeleteRequest) -> Result<(), SecretError> {
244 ensure_scoped_secret_access_allowed("delete", &request.id)?;
245 self.delete(&request.id).await
246 }
247
248 async fn list(&self, _prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
249 Err(SecretError::Unsupported {
250 provider: "keyring".to_string(),
251 operation: "list",
252 })
253 }
254
255 fn namespace(&self) -> &str {
256 self.service()
257 }
258
259 fn supports_versions(&self) -> bool {
260 false
261 }
262}
263
264fn backend_error(operation: &str, error: NativeKeyringError) -> SecretError {
265 SecretError::Backend {
266 provider: "keyring".to_string(),
267 message: format!("failed to {operation} keyring credential: {error}"),
268 }
269}
270
271fn account_name(id: &SecretId) -> String {
272 let mut account = String::new();
273 if !id.namespace.is_empty() {
274 account.push_str(&sanitize_component(&id.namespace));
275 account.push('/');
276 }
277 account.push_str(&sanitize_component(&id.name));
278 match id.version {
279 super::SecretVersion::Latest => {}
280 super::SecretVersion::Exact(version) => {
281 account.push('#');
282 account.push('v');
283 account.push_str(&version.to_string());
284 }
285 }
286 account
287}
288
289fn sanitize_component(value: &str) -> String {
290 let normalized = value
291 .chars()
292 .map(|ch| {
293 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':' | '/') {
294 ch
295 } else {
296 '_'
297 }
298 })
299 .collect::<String>();
300 if normalized.is_empty() {
301 "_".to_string()
302 } else {
303 normalized
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn native_keyring_round_trips_and_lists_service_users() {
313 let keyring = NativeKeyring::with_store(
314 "harn.native-test",
315 keyring_core::mock::Store::new().unwrap(),
316 );
317 keyring.set_string("alpha", "one").unwrap();
318 keyring.set_string("beta", "two").unwrap();
319
320 assert_eq!(keyring.get_string("alpha").unwrap().as_deref(), Some("one"));
321 assert_eq!(keyring.list().unwrap(), vec!["alpha", "beta"]);
322 assert!(keyring.delete("alpha").unwrap());
323 assert!(!keyring.delete("alpha").unwrap());
324 }
325}