1use std::any::Any;
2use std::collections::HashMap;
3use std::sync::{Arc, RwLock, Weak};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use dashmap::DashMap;
7use log::{debug, error};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::credential::{CredId, CredKey};
12use crate::{
13 Entry,
14 Error::{Invalid, PlatformFailure},
15 Result,
16 api::{CredentialPersistence, CredentialStoreApi},
17 attributes::parse_attributes,
18};
19
20#[derive(Debug, Serialize, Deserialize)]
22pub struct CredValue {
23 pub secret: Vec<u8>,
24 pub comment: Option<String>,
25 pub creation_date: Option<String>,
26}
27
28impl CredValue {
29 pub fn new(secret: &[u8]) -> Self {
30 CredValue {
31 secret: secret.to_vec(),
32 comment: None,
33 creation_date: None,
34 }
35 }
36
37 pub fn new_ambiguous(comment: &str) -> CredValue {
38 CredValue {
39 secret: vec![],
40 comment: Some(comment.to_string()),
41 creation_date: Some(chrono::Local::now().to_rfc2822()),
42 }
43 }
44}
45
46pub type CredMap = DashMap<CredId, DashMap<String, CredValue>>;
48
49pub struct SelfRef {
58 inner_store: Weak<Store>,
59}
60
61pub struct Store {
67 pub id: String,
68 pub creds: CredMap,
69 pub backing: Option<String>, pub self_ref: RwLock<SelfRef>,
71}
72
73impl std::fmt::Debug for Store {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("Store")
76 .field("vendor", &self.vendor())
77 .field("id", &self.id)
78 .field("backing", &self.backing)
79 .field("cred-count", &self.creds.len())
80 .finish()
81 }
82}
83
84impl Drop for Store {
85 fn drop(&mut self) {
86 if self.backing.is_none() {
87 debug!("dropping store {self:?}")
88 } else {
89 debug!("Saving store {self:?} on drop...");
90 match self.save() {
91 Ok(_) => debug!("Save of store {self:?} completed"),
92 Err(e) => error!("Save of store {self:?} failed: {e:?}"),
93 }
94 }
95 }
96}
97
98impl Store {
99 pub fn new() -> Result<Arc<Self>> {
103 Ok(Self::new_internal(DashMap::new(), None))
104 }
105
106 pub fn new_with_configuration(config: &HashMap<&str, &str>) -> Result<Arc<Self>> {
111 let mods = parse_attributes(&["backing-file", "*persist"], Some(config))?;
112 if let Some(path) = mods.get("backing-file") {
113 Self::new_with_backing(path)
114 } else if let Some(persist) = mods.get("persist") {
115 if persist == "true" {
116 let dir = std::env::temp_dir();
117 let path = dir.join("keyring-sample-store.ron");
118 Self::new_with_backing(path.to_str().expect("Invalid backing path"))
119 } else {
120 Self::new()
121 }
122 } else {
123 Self::new()
124 }
125 }
126
127 pub fn new_with_backing(path: &str) -> Result<Arc<Self>> {
133 Ok(Self::new_internal(
134 Self::load_credentials(path)?,
135 Some(String::from(path)),
136 ))
137 }
138
139 pub fn save(&self) -> Result<()> {
151 if self.backing.is_none() {
152 return Ok(());
153 };
154 let content = ron::ser::to_string_pretty(&self.creds, ron::ser::PrettyConfig::new())
155 .map_err(|e| PlatformFailure(Box::from(e)))?;
156 std::fs::write(self.backing.as_ref().unwrap(), content)
157 .map_err(|e| PlatformFailure(Box::from(e)))?;
158 Ok(())
159 }
160
161 pub fn new_internal(creds: CredMap, backing: Option<String>) -> Arc<Self> {
163 let store = Store {
164 id: format!(
165 "Crate version {}, Instantiated at {}",
166 env!("CARGO_PKG_VERSION"),
167 SystemTime::now()
168 .duration_since(UNIX_EPOCH)
169 .unwrap_or_else(|_| Duration::new(0, 0))
170 .as_secs_f64()
171 ),
172 creds,
173 backing,
174 self_ref: RwLock::new(SelfRef {
175 inner_store: Weak::new(),
176 }),
177 };
178 debug!("Created new store: {store:?}");
179 let result = Arc::new(store);
180 result.set_store(result.clone());
181 result
182 }
183
184 pub fn load_credentials(path: &str) -> Result<CredMap> {
188 match std::fs::exists(path) {
189 Ok(true) => match std::fs::read_to_string(path) {
190 Ok(s) => Ok(ron::de::from_str(&s).map_err(|e| PlatformFailure(Box::from(e)))?),
191 Err(e) => Err(PlatformFailure(Box::from(e))),
192 },
193 Ok(false) => Ok(DashMap::new()),
194 Err(e) => Err(Invalid("Invalid path".to_string(), e.to_string())),
195 }
196 }
197
198 fn get_store(&self) -> Arc<Store> {
199 self.self_ref
200 .read()
201 .expect("RwLock bug at get!")
202 .inner_store
203 .upgrade()
204 .expect("Arc bug at get!")
205 }
206
207 fn set_store(&self, store: Arc<Store>) {
208 let mut guard = self.self_ref.write().expect("RwLock bug at set!");
209 guard.inner_store = Arc::downgrade(&store);
210 }
211}
212
213impl CredentialStoreApi for Store {
214 fn vendor(&self) -> String {
216 String::from("Sample store, https://crates.io/crates/keyring-core")
217 }
218
219 fn id(&self) -> String {
224 self.id.clone()
225 }
226
227 fn build(
236 &self,
237 service: &str,
238 user: &str,
239 mods: Option<&HashMap<&str, &str>>,
240 ) -> Result<Entry> {
241 let id = CredId {
242 service: service.to_owned(),
243 user: user.to_owned(),
244 };
245 let key = CredKey {
246 store: self.get_store(),
247 id: id.clone(),
248 uuid: None,
249 };
250 if let Some(force_create) = parse_attributes(&["force-create"], mods)?.get("force-create") {
251 let uuid = Uuid::new_v4().to_string();
252 let value = CredValue::new_ambiguous(force_create);
253 match self.creds.get(&id) {
254 None => {
255 let creds = DashMap::new();
256 creds.insert(uuid, value);
257 self.creds.insert(id, creds);
258 }
259 Some(creds) => {
260 creds.value().insert(uuid, value);
261 }
262 };
263 }
264 Ok(Entry {
265 inner: Arc::new(key),
266 })
267 }
268
269 fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
277 let mut result: Vec<Entry> = Vec::new();
278 let svc = regex::Regex::new(spec.get("service").unwrap_or(&""))
279 .map_err(|e| Invalid("service regex".to_string(), e.to_string()))?;
280 let usr = regex::Regex::new(spec.get("user").unwrap_or(&""))
281 .map_err(|e| Invalid("user regex".to_string(), e.to_string()))?;
282 let comment = regex::Regex::new(spec.get("uuid").unwrap_or(&""))
283 .map_err(|e| Invalid("comment regex".to_string(), e.to_string()))?;
284 let uuid = regex::Regex::new(spec.get("uuid").unwrap_or(&""))
285 .map_err(|e| Invalid("uuid regex".to_string(), e.to_string()))?;
286 let store = self.get_store();
287 for pair in self.creds.iter() {
288 if !svc.is_match(pair.key().service.as_str()) {
289 continue;
290 }
291 if !usr.is_match(pair.key().user.as_str()) {
292 continue;
293 }
294 for cred in pair.value().iter() {
295 if !uuid.is_match(cred.key()) {
296 continue;
297 }
298 if spec.get("comment").is_some() {
299 if cred.value().comment.is_none() {
300 continue;
301 }
302 if !comment.is_match(cred.value().comment.as_ref().unwrap()) {
303 continue;
304 }
305 }
306 result.push(Entry {
307 inner: Arc::new(CredKey {
308 store: store.clone(),
309 id: pair.key().clone(),
310 uuid: Some(cred.key().clone()),
311 }),
312 })
313 }
314 }
315 Ok(result)
316 }
317
318 fn as_any(&self) -> &dyn Any {
320 self
321 }
322
323 fn persistence(&self) -> CredentialPersistence {
328 if self.backing.is_none() {
329 CredentialPersistence::ProcessOnly
330 } else {
331 CredentialPersistence::UntilDelete
332 }
333 }
334
335 fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 std::fmt::Debug::fmt(self, f)
338 }
339}