1use std::collections::HashMap;
4use std::sync::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)]
191pub struct KeyringStore;
192
193impl KeyringStore {
194 #[must_use]
197 pub const fn new() -> Self {
198 Self
199 }
200}
201
202#[async_trait]
203impl CredentialStore for KeyringStore {
204 async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError> {
205 let service = service.to_string();
206 let account = account.to_string();
207 tokio::task::spawn_blocking(move || -> Result<SecretString, CredentialError> {
208 let entry = keyring::Entry::new(&service, &account)
209 .map_err(|e| CredentialError::Keychain(e.to_string()))?;
210 match entry.get_password() {
211 Ok(pw) => Ok(SecretString::from(pw)),
212 Err(keyring::Error::NoEntry) => {
213 Err(CredentialError::NotFound { name: format!("{service}/{account}") })
214 }
215 Err(e) => Err(CredentialError::Keychain(e.to_string())),
216 }
217 })
218 .await
219 .map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
220 }
221
222 async fn set(
223 &self,
224 service: &str,
225 account: &str,
226 secret: SecretString,
227 ) -> Result<(), CredentialError> {
228 let service = service.to_string();
229 let account = account.to_string();
230 tokio::task::spawn_blocking(move || -> Result<(), CredentialError> {
231 let entry = keyring::Entry::new(&service, &account)
232 .map_err(|e| CredentialError::Keychain(e.to_string()))?;
233 entry
234 .set_password(secret.expose_secret())
235 .map_err(|e| CredentialError::Keychain(e.to_string()))
236 })
237 .await
238 .map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
239 }
240
241 async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError> {
242 let service = service.to_string();
243 let account = account.to_string();
244 tokio::task::spawn_blocking(move || -> Result<(), CredentialError> {
245 let entry = keyring::Entry::new(&service, &account)
246 .map_err(|e| CredentialError::Keychain(e.to_string()))?;
247 match entry.delete_credential() {
248 Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
249 Err(e) => Err(CredentialError::Keychain(e.to_string())),
250 }
251 })
252 .await
253 .map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
254 }
255}