use std::any::Any;
use std::collections::HashMap;
use std::sync::{Arc, RwLock, Weak};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use dashmap::DashMap;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::credential::{CredId, CredKey};
use crate::{
Entry,
Error::{Invalid, PlatformFailure},
Result,
api::{CredentialPersistence, CredentialStoreApi},
attributes::parse_attributes,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct CredValue {
pub secret: Vec<u8>,
pub comment: Option<String>,
pub creation_date: Option<String>,
}
impl CredValue {
pub fn new(secret: &[u8]) -> Self {
CredValue {
secret: secret.to_vec(),
comment: None,
creation_date: None,
}
}
pub fn new_ambiguous(comment: &str) -> CredValue {
CredValue {
secret: vec![],
comment: Some(comment.to_string()),
creation_date: Some(chrono::Local::now().to_rfc2822()),
}
}
}
pub type CredMap = DashMap<CredId, DashMap<String, CredValue>>;
pub struct SelfRef {
inner_store: Weak<Store>,
}
pub struct Store {
pub id: String,
pub creds: CredMap,
pub backing: Option<String>, pub self_ref: RwLock<SelfRef>,
}
impl std::fmt::Debug for Store {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Store")
.field("vendor", &self.vendor())
.field("id", &self.id)
.field("backing", &self.backing)
.field("cred-count", &self.creds.len())
.finish()
}
}
impl Drop for Store {
fn drop(&mut self) {
if self.backing.is_none() {
debug!("dropping store {self:?}")
} else {
debug!("Saving store {self:?} on drop...");
match self.save() {
Ok(_) => debug!("Save of store {self:?} completed"),
Err(e) => error!("Save of store {self:?} failed: {e:?}"),
}
}
}
}
impl Store {
pub fn new() -> Result<Arc<Self>> {
Ok(Self::new_internal(DashMap::new(), None))
}
pub fn new_with_configuration(config: &HashMap<&str, &str>) -> Result<Arc<Self>> {
let mods = parse_attributes(&["backing-file", "*persist"], Some(config))?;
if let Some(path) = mods.get("backing-file") {
Self::new_with_backing(path)
} else if let Some(persist) = mods.get("persist") {
if persist == "true" {
let dir = std::env::temp_dir();
let path = dir.join("keyring-sample-store.ron");
Self::new_with_backing(path.to_str().expect("Invalid backing path"))
} else {
Self::new()
}
} else {
Self::new()
}
}
pub fn new_with_backing(path: &str) -> Result<Arc<Self>> {
Ok(Self::new_internal(
Self::load_credentials(path)?,
Some(String::from(path)),
))
}
pub fn save(&self) -> Result<()> {
if self.backing.is_none() {
return Ok(());
};
let content = ron::ser::to_string_pretty(&self.creds, ron::ser::PrettyConfig::new())
.map_err(|e| PlatformFailure(Box::from(e)))?;
std::fs::write(self.backing.as_ref().unwrap(), content)
.map_err(|e| PlatformFailure(Box::from(e)))?;
Ok(())
}
pub fn new_internal(creds: CredMap, backing: Option<String>) -> Arc<Self> {
let store = Store {
id: format!(
"Crate version {}, Instantiated at {}",
env!("CARGO_PKG_VERSION"),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::new(0, 0))
.as_secs_f64()
),
creds,
backing,
self_ref: RwLock::new(SelfRef {
inner_store: Weak::new(),
}),
};
debug!("Created new store: {store:?}");
let result = Arc::new(store);
result.set_store(result.clone());
result
}
pub fn load_credentials(path: &str) -> Result<CredMap> {
match std::fs::exists(path) {
Ok(true) => match std::fs::read_to_string(path) {
Ok(s) => Ok(ron::de::from_str(&s).map_err(|e| PlatformFailure(Box::from(e)))?),
Err(e) => Err(PlatformFailure(Box::from(e))),
},
Ok(false) => Ok(DashMap::new()),
Err(e) => Err(Invalid("Invalid path".to_string(), e.to_string())),
}
}
fn get_store(&self) -> Arc<Store> {
self.self_ref
.read()
.expect("RwLock bug at get!")
.inner_store
.upgrade()
.expect("Arc bug at get!")
}
fn set_store(&self, store: Arc<Store>) {
let mut guard = self.self_ref.write().expect("RwLock bug at set!");
guard.inner_store = Arc::downgrade(&store);
}
}
impl CredentialStoreApi for Store {
fn vendor(&self) -> String {
String::from("Sample store, https://crates.io/crates/keyring-core")
}
fn id(&self) -> String {
self.id.clone()
}
fn build(
&self,
service: &str,
user: &str,
mods: Option<&HashMap<&str, &str>>,
) -> Result<Entry> {
let id = CredId {
service: service.to_owned(),
user: user.to_owned(),
};
let key = CredKey {
store: self.get_store(),
id: id.clone(),
uuid: None,
};
if let Some(force_create) = parse_attributes(&["force-create"], mods)?.get("force-create") {
let uuid = Uuid::new_v4().to_string();
let value = CredValue::new_ambiguous(force_create);
match self.creds.get(&id) {
None => {
let creds = DashMap::new();
creds.insert(uuid, value);
self.creds.insert(id, creds);
}
Some(creds) => {
creds.value().insert(uuid, value);
}
};
}
Ok(Entry {
inner: Arc::new(key),
})
}
fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
let spec = parse_attributes(&["service", "user", "uuid", "comment"], Some(spec))?;
let mut result: Vec<Entry> = Vec::new();
let empty = String::new();
let svc = regex::Regex::new(spec.get("service").unwrap_or(&empty))
.map_err(|e| Invalid("service regex".to_string(), e.to_string()))?;
let usr = regex::Regex::new(spec.get("user").unwrap_or(&empty))
.map_err(|e| Invalid("user regex".to_string(), e.to_string()))?;
let comment = regex::Regex::new(spec.get("comment").unwrap_or(&empty))
.map_err(|e| Invalid("comment regex".to_string(), e.to_string()))?;
let uuid = regex::Regex::new(spec.get("uuid").unwrap_or(&empty))
.map_err(|e| Invalid("uuid regex".to_string(), e.to_string()))?;
let store = self.get_store();
for pair in self.creds.iter() {
if !svc.is_match(pair.key().service.as_str()) {
continue;
}
if !usr.is_match(pair.key().user.as_str()) {
continue;
}
for cred in pair.value().iter() {
if !uuid.is_match(cred.key()) {
continue;
}
if spec.contains_key("comment") {
if cred.value().comment.is_none() {
continue;
}
if !comment.is_match(cred.value().comment.as_ref().unwrap()) {
continue;
}
}
result.push(Entry {
inner: Arc::new(CredKey {
store: store.clone(),
id: pair.key().clone(),
uuid: Some(cred.key().clone()),
}),
})
}
}
Ok(result)
}
fn as_any(&self) -> &dyn Any {
self
}
fn persistence(&self) -> CredentialPersistence {
if self.backing.is_none() {
CredentialPersistence::ProcessOnly
} else {
CredentialPersistence::UntilDelete
}
}
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}