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<()> {
149 if self.backing.is_none() {
150 return Ok(());
151 };
152 let content = ron::ser::to_string_pretty(&self.creds, ron::ser::PrettyConfig::new())
153 .map_err(|e| PlatformFailure(Box::from(e)))?;
154 std::fs::write(self.backing.as_ref().unwrap(), content)
155 .map_err(|e| PlatformFailure(Box::from(e)))?;
156 Ok(())
157 }
158
159 pub fn new_internal(creds: CredMap, backing: Option<String>) -> Arc<Self> {
161 let store = Store {
162 id: format!(
163 "Crate version {}, Instantiated at {}",
164 env!("CARGO_PKG_VERSION"),
165 SystemTime::now()
166 .duration_since(UNIX_EPOCH)
167 .unwrap_or_else(|_| Duration::new(0, 0))
168 .as_secs_f64()
169 ),
170 creds,
171 backing,
172 self_ref: RwLock::new(SelfRef {
173 inner_store: Weak::new(),
174 }),
175 };
176 debug!("Created new store: {store:?}");
177 let result = Arc::new(store);
178 result.set_store(result.clone());
179 result
180 }
181
182 pub fn load_credentials(path: &str) -> Result<CredMap> {
186 match std::fs::exists(path) {
187 Ok(true) => match std::fs::read_to_string(path) {
188 Ok(s) => Ok(ron::de::from_str(&s).map_err(|e| PlatformFailure(Box::from(e)))?),
189 Err(e) => Err(PlatformFailure(Box::from(e))),
190 },
191 Ok(false) => Ok(DashMap::new()),
192 Err(e) => Err(Invalid("Invalid path".to_string(), e.to_string())),
193 }
194 }
195
196 fn get_store(&self) -> Arc<Store> {
197 self.self_ref
198 .read()
199 .expect("RwLock bug at get!")
200 .inner_store
201 .upgrade()
202 .expect("Arc bug at get!")
203 }
204
205 fn set_store(&self, store: Arc<Store>) {
206 let mut guard = self.self_ref.write().expect("RwLock bug at set!");
207 guard.inner_store = Arc::downgrade(&store);
208 }
209}
210
211impl CredentialStoreApi for Store {
212 fn vendor(&self) -> String {
214 String::from("Sample store, https://crates.io/crates/keyring-core")
215 }
216
217 fn id(&self) -> String {
222 self.id.clone()
223 }
224
225 fn build(
234 &self,
235 service: &str,
236 user: &str,
237 mods: Option<&HashMap<&str, &str>>,
238 ) -> Result<Entry> {
239 let id = CredId {
240 service: service.to_owned(),
241 user: user.to_owned(),
242 };
243 let key = CredKey {
244 store: self.get_store(),
245 id: id.clone(),
246 uuid: None,
247 };
248 if let Some(force_create) = parse_attributes(&["force-create"], mods)?.get("force-create") {
249 let uuid = Uuid::new_v4().to_string();
250 let value = CredValue::new_ambiguous(force_create);
251 match self.creds.get(&id) {
252 None => {
253 let creds = DashMap::new();
254 creds.insert(uuid, value);
255 self.creds.insert(id, creds);
256 }
257 Some(creds) => {
258 creds.value().insert(uuid, value);
259 }
260 };
261 }
262 Ok(Entry {
263 inner: Arc::new(key),
264 })
265 }
266
267 fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
275 let spec = parse_attributes(&["service", "user", "uuid", "comment"], Some(spec))?;
276 let mut result: Vec<Entry> = Vec::new();
277 let empty = String::new();
278 let svc = regex::Regex::new(spec.get("service").unwrap_or(&empty))
279 .map_err(|e| Invalid("service regex".to_string(), e.to_string()))?;
280 let usr = regex::Regex::new(spec.get("user").unwrap_or(&empty))
281 .map_err(|e| Invalid("user regex".to_string(), e.to_string()))?;
282 let comment = regex::Regex::new(spec.get("comment").unwrap_or(&empty))
283 .map_err(|e| Invalid("comment regex".to_string(), e.to_string()))?;
284 let uuid = regex::Regex::new(spec.get("uuid").unwrap_or(&empty))
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.contains_key("comment") {
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}