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("id", &self.id)
77 .field("backing", &self.backing)
78 .field("cred-count", &self.creds.len())
79 .finish()
80 }
81}
82
83impl Drop for Store {
84 fn drop(&mut self) {
85 if self.backing.is_none() {
86 debug!("dropping store {self:?}")
87 } else {
88 debug!("Saving store {self:?} on drop...");
89 match self.save() {
90 Ok(_) => debug!("Save of store {self:?} completed"),
91 Err(e) => error!("Save of store {self:?} failed: {e:?}"),
92 }
93 }
94 }
95}
96
97impl Store {
98 pub fn new() -> Result<Arc<Self>> {
102 Ok(Self::new_internal(DashMap::new(), None))
103 }
104
105 pub fn new_with_configuration(config: &HashMap<&str, &str>) -> Result<Arc<Self>> {
111 match parse_attributes(&["backing-file"], Some(config))?.get("backing-file") {
112 Some(path) => Self::new_with_backing(path),
113 None => Self::new(),
114 }
115 }
116
117 pub fn new_with_backing(path: &str) -> Result<Arc<Self>> {
123 Ok(Self::new_internal(
124 Self::load_credentials(path)?,
125 Some(String::from(path)),
126 ))
127 }
128
129 pub fn save(&self) -> Result<()> {
141 if self.backing.is_none() {
142 return Ok(());
143 };
144 let content = ron::ser::to_string_pretty(&self.creds, ron::ser::PrettyConfig::new())
145 .map_err(|e| PlatformFailure(Box::from(e)))?;
146 std::fs::write(self.backing.as_ref().unwrap(), content)
147 .map_err(|e| PlatformFailure(Box::from(e)))?;
148 Ok(())
149 }
150
151 pub fn new_internal(creds: CredMap, backing: Option<String>) -> Arc<Self> {
153 let store = Store {
154 id: format!(
155 "Crate version {}, Instantiated at {}",
156 env!("CARGO_PKG_VERSION"),
157 SystemTime::now()
158 .duration_since(UNIX_EPOCH)
159 .unwrap_or_else(|_| Duration::new(0, 0))
160 .as_secs_f64()
161 ),
162 creds,
163 backing,
164 self_ref: RwLock::new(SelfRef {
165 inner_store: Weak::new(),
166 }),
167 };
168 debug!("Created new store: {store:?}");
169 let result = Arc::new(store);
170 result.set_store(result.clone());
171 result
172 }
173
174 pub fn load_credentials(path: &str) -> Result<CredMap> {
178 match std::fs::exists(path) {
179 Ok(true) => match std::fs::read_to_string(path) {
180 Ok(s) => Ok(ron::de::from_str(&s).map_err(|e| PlatformFailure(Box::from(e)))?),
181 Err(e) => Err(PlatformFailure(Box::from(e))),
182 },
183 Ok(false) => Ok(DashMap::new()),
184 Err(e) => Err(Invalid("Invalid path".to_string(), e.to_string())),
185 }
186 }
187
188 fn get_store(&self) -> Arc<Store> {
189 self.self_ref
190 .read()
191 .expect("RwLock bug at get!")
192 .inner_store
193 .upgrade()
194 .expect("Arc bug at get!")
195 }
196
197 fn set_store(&self, store: Arc<Store>) {
198 let mut guard = self.self_ref.write().expect("RwLock bug at set!");
199 guard.inner_store = Arc::downgrade(&store);
200 }
201}
202
203impl CredentialStoreApi for Store {
204 fn vendor(&self) -> String {
206 String::from("In-memory sample store, https://crates.io/crates/keyring-core")
207 }
208
209 fn id(&self) -> String {
214 self.id.clone()
215 }
216
217 fn build(
226 &self,
227 service: &str,
228 user: &str,
229 mods: Option<&HashMap<&str, &str>>,
230 ) -> Result<Entry> {
231 let id = CredId {
232 service: service.to_owned(),
233 user: user.to_owned(),
234 };
235 let key = CredKey {
236 store: self.get_store(),
237 id: id.clone(),
238 uuid: None,
239 };
240 if let Some(force_create) = parse_attributes(&["force-create"], mods)?.get("force-create") {
241 let uuid = Uuid::new_v4().to_string();
242 let value = CredValue::new_ambiguous(force_create);
243 match self.creds.get(&id) {
244 None => {
245 let creds = DashMap::new();
246 creds.insert(uuid, value);
247 self.creds.insert(id, creds);
248 }
249 Some(creds) => {
250 creds.value().insert(uuid, value);
251 }
252 };
253 }
254 Ok(Entry {
255 inner: Arc::new(key),
256 })
257 }
258
259 fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
267 let mut result: Vec<Entry> = Vec::new();
268 let svc = regex::Regex::new(spec.get("service").unwrap_or(&""))
269 .map_err(|e| Invalid("service regex".to_string(), e.to_string()))?;
270 let usr = regex::Regex::new(spec.get("user").unwrap_or(&""))
271 .map_err(|e| Invalid("user regex".to_string(), e.to_string()))?;
272 let comment = regex::Regex::new(spec.get("uuid").unwrap_or(&""))
273 .map_err(|e| Invalid("comment regex".to_string(), e.to_string()))?;
274 let uuid = regex::Regex::new(spec.get("uuid").unwrap_or(&""))
275 .map_err(|e| Invalid("uuid regex".to_string(), e.to_string()))?;
276 let store = self.get_store();
277 for pair in self.creds.iter() {
278 if !svc.is_match(pair.key().service.as_str()) {
279 continue;
280 }
281 if !usr.is_match(pair.key().user.as_str()) {
282 continue;
283 }
284 for cred in pair.value().iter() {
285 if !uuid.is_match(cred.key()) {
286 continue;
287 }
288 if spec.get("comment").is_some() {
289 if cred.value().comment.is_none() {
290 continue;
291 }
292 if !comment.is_match(cred.value().comment.as_ref().unwrap()) {
293 continue;
294 }
295 }
296 result.push(Entry {
297 inner: Arc::new(CredKey {
298 store: store.clone(),
299 id: pair.key().clone(),
300 uuid: Some(cred.key().clone()),
301 }),
302 })
303 }
304 }
305 Ok(result)
306 }
307
308 fn as_any(&self) -> &dyn Any {
310 self
311 }
312
313 fn persistence(&self) -> CredentialPersistence {
318 if self.backing.is_none() {
319 CredentialPersistence::ProcessOnly
320 } else {
321 CredentialPersistence::UntilDelete
322 }
323 }
324
325 fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 std::fmt::Debug::fmt(self, f)
328 }
329}