1use std::collections::HashMap;
4use std::sync::{Arc, OnceLock, RwLock};
5
6use async_trait::async_trait;
7use secrecy::{ExposeSecret, SecretString};
8
9use crate::error::CredentialError;
10
11#[async_trait]
18pub trait CredentialStore: Send + Sync + 'static {
19 async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError>;
23
24 async fn set(
28 &self,
29 service: &str,
30 account: &str,
31 secret: SecretString,
32 ) -> Result<(), CredentialError>;
33
34 async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError>;
38}
39
40#[derive(Default)]
47pub struct MemoryStore {
48 inner: RwLock<HashMap<(String, String), SecretString>>,
49}
50
51impl std::fmt::Debug for MemoryStore {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("MemoryStore").finish_non_exhaustive()
54 }
55}
56
57impl MemoryStore {
58 #[must_use]
60 pub fn new() -> Self {
61 Self::default()
62 }
63}
64
65#[async_trait]
66impl CredentialStore for MemoryStore {
67 async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError> {
68 let map = self.inner.read().map_err(|_| poisoned())?;
69 map.get(&(service.to_string(), account.to_string()))
70 .cloned()
71 .ok_or_else(|| CredentialError::NotFound { name: format!("{service}/{account}") })
72 }
73
74 async fn set(
75 &self,
76 service: &str,
77 account: &str,
78 secret: SecretString,
79 ) -> Result<(), CredentialError> {
80 {
81 let mut map = self.inner.write().map_err(|_| poisoned())?;
82 map.insert((service.to_string(), account.to_string()), secret);
83 }
84 Ok(())
85 }
86
87 async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError> {
88 {
89 let mut map = self.inner.write().map_err(|_| poisoned())?;
90 map.remove(&(service.to_string(), account.to_string()));
91 }
92 Ok(())
93 }
94}
95
96fn poisoned() -> CredentialError {
97 CredentialError::Keychain("in-memory lock poisoned".to_string())
98}
99
100#[derive(Debug, Default)]
111pub struct EnvStore;
112
113impl EnvStore {
114 #[must_use]
116 pub const fn new() -> Self {
117 Self
118 }
119}
120
121#[async_trait]
122impl CredentialStore for EnvStore {
123 async fn get(&self, _service: &str, account: &str) -> Result<SecretString, CredentialError> {
124 std::env::var(account)
125 .map(SecretString::from)
126 .map_err(|_| CredentialError::NotFound { name: account.to_string() })
127 }
128
129 async fn set(&self, _: &str, _: &str, _: SecretString) -> Result<(), CredentialError> {
130 Err(CredentialError::ReadOnly)
131 }
132
133 async fn delete(&self, _: &str, _: &str) -> Result<(), CredentialError> {
134 Err(CredentialError::ReadOnly)
135 }
136}
137
138pub struct LiteralStore {
146 secret: SecretString,
147}
148
149impl std::fmt::Debug for LiteralStore {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("LiteralStore").finish_non_exhaustive()
152 }
153}
154
155impl LiteralStore {
156 #[must_use]
158 pub const fn new(secret: SecretString) -> Self {
159 Self { secret }
160 }
161}
162
163#[async_trait]
164impl CredentialStore for LiteralStore {
165 async fn get(&self, _: &str, _: &str) -> Result<SecretString, CredentialError> {
166 Ok(self.secret.clone())
170 }
171
172 async fn set(&self, _: &str, _: &str, _: SecretString) -> Result<(), CredentialError> {
173 Err(CredentialError::ReadOnly)
174 }
175
176 async fn delete(&self, _: &str, _: &str) -> Result<(), CredentialError> {
177 Err(CredentialError::ReadOnly)
178 }
179}
180
181#[derive(Debug, Default)]
192pub struct KeyringStore;
193
194impl KeyringStore {
195 #[must_use]
198 pub const fn new() -> Self {
199 Self
200 }
201}
202
203#[async_trait]
204impl CredentialStore for KeyringStore {
205 async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError> {
206 let service = service.to_string();
207 let account = account.to_string();
208 tokio::task::spawn_blocking(move || -> Result<SecretString, CredentialError> {
209 match platform_entry(&service, &account)?.get_password() {
210 Ok(pw) => Ok(SecretString::from(pw)),
211 Err(keyring_core::Error::NoEntry) => {
212 Err(CredentialError::NotFound { name: format!("{service}/{account}") })
213 }
214 Err(e) => Err(CredentialError::Keychain(e.to_string())),
215 }
216 })
217 .await
218 .map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
219 }
220
221 async fn set(
222 &self,
223 service: &str,
224 account: &str,
225 secret: SecretString,
226 ) -> Result<(), CredentialError> {
227 let service = service.to_string();
228 let account = account.to_string();
229 tokio::task::spawn_blocking(move || -> Result<(), CredentialError> {
230 platform_entry(&service, &account)?
231 .set_password(secret.expose_secret())
232 .map_err(|e| CredentialError::Keychain(e.to_string()))
233 })
234 .await
235 .map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
236 }
237
238 async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError> {
239 let service = service.to_string();
240 let account = account.to_string();
241 tokio::task::spawn_blocking(move || -> Result<(), CredentialError> {
242 match platform_entry(&service, &account)?.delete_credential() {
243 Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(()),
244 Err(e) => Err(CredentialError::Keychain(e.to_string())),
245 }
246 })
247 .await
248 .map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
249 }
250}
251
252fn platform_entry(service: &str, account: &str) -> Result<keyring_core::Entry, CredentialError> {
256 platform_store()?
257 .build(service, account, None)
258 .map_err(|e| CredentialError::Keychain(e.to_string()))
259}
260
261static PLATFORM_STORE: OnceLock<Arc<keyring_core::CredentialStore>> = OnceLock::new();
262
263fn platform_store() -> Result<&'static Arc<keyring_core::CredentialStore>, CredentialError> {
266 if let Some(store) = PLATFORM_STORE.get() {
267 return Ok(store);
268 }
269 let store = new_platform_store().map_err(|e| CredentialError::Keychain(e.to_string()))?;
270 Ok(PLATFORM_STORE.get_or_init(|| store))
271}
272
273#[cfg(target_os = "macos")]
274fn new_platform_store() -> keyring_core::Result<Arc<keyring_core::CredentialStore>> {
275 Ok(apple_native_keyring_store::keychain::Store::new()?)
276}
277
278#[cfg(target_os = "windows")]
279fn new_platform_store() -> keyring_core::Result<Arc<keyring_core::CredentialStore>> {
280 Ok(windows_native_keyring_store::Store::new()?)
281}
282
283#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios", target_os = "android"))))]
284fn new_platform_store() -> keyring_core::Result<Arc<keyring_core::CredentialStore>> {
285 Ok(dbus_secret_service_keyring_store::Store::new()?)
286}
287
288#[cfg(not(any(
289 target_os = "macos",
290 target_os = "windows",
291 all(unix, not(any(target_os = "ios", target_os = "android")))
292)))]
293fn new_platform_store() -> keyring_core::Result<Arc<keyring_core::CredentialStore>> {
294 Err(keyring_core::Error::NoDefaultStore)
295}