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