use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::api::{CredentialApi, CredentialStoreApi};
use crate::{Credential, CredentialPersistence, Entry, Error, Result};
#[derive(Debug)]
pub struct Cred {
pub specifiers: (String, String),
pub inner: Mutex<RefCell<CredData>>,
}
#[derive(Debug, Default)]
pub struct CredData {
pub secret: Option<Vec<u8>>,
pub error: Option<Error>,
}
impl CredentialApi for Cred {
fn set_secret(&self, secret: &[u8]) -> Result<()> {
let mut inner = self
.inner
.lock()
.expect("Can't access mock data for set_secret: please report a bug!");
let data = inner.get_mut();
let err = data.error.take();
match err {
None => {
data.secret = Some(secret.to_vec());
Ok(())
}
Some(err) => Err(err),
}
}
fn get_secret(&self) -> Result<Vec<u8>> {
let mut inner = self
.inner
.lock()
.expect("Can't access mock data for get: please report a bug!");
let data = inner.get_mut();
let err = data.error.take();
match err {
None => match &data.secret {
None => Err(Error::NoEntry),
Some(val) => Ok(val.clone()),
},
Some(err) => Err(err),
}
}
fn delete_credential(&self) -> Result<()> {
let mut inner = self
.inner
.lock()
.expect("Can't access mock data for delete: please report a bug!");
let data = inner.get_mut();
let err = data.error.take();
match err {
None => match data.secret {
Some(_) => {
data.secret = None;
Ok(())
}
None => Err(Error::NoEntry),
},
Some(err) => Err(err),
}
}
fn get_credential(&self) -> Result<Option<Arc<Credential>>> {
let mut inner = self
.inner
.lock()
.expect("Can't access mock data for get_credential: please report a bug!");
let data = inner.get_mut();
let err = data.error.take();
match err {
None => match data.secret {
Some(_) => Ok(None),
None => Err(Error::NoEntry),
},
Some(err) => Err(err),
}
}
fn get_specifiers(&self) -> Option<(String, String)> {
Some(self.specifiers.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
impl Cred {
pub fn set_error(&self, err: Error) {
let mut inner = self
.inner
.lock()
.expect("Can't access mock data for set_error: please report a bug!");
let data = inner.get_mut();
data.error = Some(err);
}
}
pub struct Store {
pub id: String,
pub inner: Mutex<RefCell<Vec<Arc<Cred>>>>,
}
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)
.finish()
}
}
impl Store {
pub fn new() -> Result<Arc<Self>> {
Ok(Arc::new(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()
),
inner: Mutex::new(RefCell::new(Vec::new())),
}))
}
}
impl CredentialStoreApi for Store {
fn vendor(&self) -> String {
String::from("Mock 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> {
if mods.is_some_and(|m| !m.is_empty()) {
let msg = "The mock store doesn't allow entry modifiers";
return Err(Error::NotSupportedByStore(msg.to_string()));
}
let mut inner = self
.inner
.lock()
.expect("Can't access mock store data: please report a bug!");
let creds = inner.get_mut();
for cred in creds.iter() {
if service == cred.specifiers.0 && user == cred.specifiers.1 {
return Ok(Entry {
inner: cred.clone(),
});
}
}
let cred = Arc::new(Cred {
specifiers: (service.to_string(), user.to_string()),
inner: Mutex::new(RefCell::new(Default::default())),
});
creds.push(cred.clone());
Ok(Entry { inner: cred })
}
fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
let mut result: Vec<Entry> = Vec::new();
let svc = spec.get("service").unwrap_or(&"");
let usr = spec.get("user").unwrap_or(&"");
let mut inner = self
.inner
.lock()
.expect("Can't access mock store data: please report a bug!");
let creds = inner.get_mut();
for cred in creds.iter() {
if !cred.specifiers.0.as_str().contains(svc) {
continue;
}
if !cred.specifiers.1.as_str().contains(usr) {
continue;
}
result.push(Entry {
inner: cred.clone(),
});
}
Ok(result)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn persistence(&self) -> CredentialPersistence {
CredentialPersistence::ProcessOnly
}
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Once};
use super::{Cred, HashMap, Store};
use crate::{CredentialPersistence, CredentialStore, Entry, Error, get_default_store};
static SET_STORE: Once = Once::new();
fn usually_goes_in_main() {
let _ = env_logger::builder().is_test(true).try_init();
crate::set_default_store(Store::new().unwrap());
}
#[test]
fn test_store_methods() {
SET_STORE.call_once(usually_goes_in_main);
let store = get_default_store().unwrap();
let vendor1 = store.vendor();
let id1 = store.id();
let vendor2 = store.vendor();
let id2 = store.id();
assert_eq!(vendor1, vendor2);
assert_eq!(id1, id2);
let store2: Arc<CredentialStore> = Store::new().unwrap();
let vendor3 = store2.vendor();
let id3 = store2.id();
assert_eq!(vendor1, vendor3);
assert_ne!(id1, id3);
}
fn entry_new(service: &str, user: &str) -> Entry {
SET_STORE.call_once(usually_goes_in_main);
Entry::new(service, user).unwrap_or_else(|err| {
panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
})
}
fn generate_random_string() -> String {
use fastrand;
use std::iter::repeat_with;
repeat_with(fastrand::alphanumeric).take(30).collect()
}
fn generate_random_bytes() -> Vec<u8> {
use fastrand;
use std::iter::repeat_with;
repeat_with(|| fastrand::u8(..)).take(24).collect()
}
fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
entry
.set_password(in_pass)
.unwrap_or_else(|err| panic!("Can't set password: {case}: {err:?}"));
let out_pass = entry
.get_password()
.unwrap_or_else(|err| panic!("Can't get password: {case}: {err:?}"));
assert_eq!(
in_pass, out_pass,
"Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
)
}
fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
test_round_trip_no_delete(case, entry, in_pass);
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete password: {case}: {err:?}"));
let password = entry.get_password();
assert!(matches!(password, Err(Error::NoEntry)));
}
pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
entry
.set_secret(in_secret)
.unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
let out_secret = entry
.get_secret()
.unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
assert_eq!(
in_secret, &out_secret,
"Secrets don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
);
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete credential for {case}: {err:?}"));
let secret = entry.get_secret();
assert!(matches!(secret, Err(Error::NoEntry)));
}
#[test]
fn test_empty_service_and_user() {
let name = generate_random_string();
let in_pass = "value doesn't matter";
test_round_trip("empty user", &entry_new(&name, ""), in_pass);
test_round_trip("empty service", &entry_new("", &name), in_pass);
test_round_trip("empty service and user", &entry_new("", ""), in_pass);
}
#[test]
fn test_empty_password() {
let name = generate_random_string();
let in_pass = "";
test_round_trip("empty password", &entry_new(&name, &name), in_pass);
}
#[test]
fn test_missing_entry() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
}
#[test]
fn test_round_trip_ascii_password() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
test_round_trip("ascii password", &entry, "test ascii password");
}
#[test]
fn test_round_trip_non_ascii_password() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
}
#[test]
fn test_entries_with_same_and_different_specifiers() {
let name1 = generate_random_string();
let name2 = generate_random_string();
let entry1 = entry_new(&name1, &name2);
let entry2 = entry_new(&name1, &name2);
let entry3 = entry_new(&name2, &name1);
entry1.set_password("test password").unwrap();
let pw2 = entry2.get_password().unwrap();
assert_eq!(pw2, "test password");
_ = entry3.get_password().unwrap_err();
entry1.delete_credential().unwrap();
_ = entry2.get_password().unwrap_err();
entry3.delete_credential().unwrap_err();
}
#[test]
fn test_get_credential_and_specifiers() {
let name = generate_random_string();
let entry1 = entry_new(&name, &name);
assert!(matches!(entry1.get_credential(), Err(Error::NoEntry)));
entry1.set_password("password for entry1").unwrap();
let wrapper = entry1.get_credential().unwrap();
let (service, user) = wrapper.get_specifiers().unwrap();
assert_eq!(service, name);
assert_eq!(user, name);
wrapper.delete_credential().unwrap();
entry1.delete_credential().unwrap_err();
wrapper.delete_credential().unwrap_err();
}
#[test]
fn test_round_trip_random_secret() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let secret = generate_random_bytes();
test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
}
#[test]
fn test_update() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
test_round_trip(
"updated non-ascii password",
&entry,
"このきれいな花は桜です",
);
}
#[test]
fn test_set_error() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let password = "test ascii password";
let mock: &Cred = entry.inner.as_any().downcast_ref().unwrap();
mock.set_error(Error::Invalid(
"mock error".to_string(),
"is an error".to_string(),
));
assert!(matches!(
entry.set_password(password),
Err(Error::Invalid(_, _))
));
entry.set_password(password).unwrap();
mock.set_error(Error::NoEntry);
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, password);
mock.set_error(Error::TooLong("mock".to_string(), 3));
assert!(matches!(
entry.delete_credential(),
Err(Error::TooLong(_, 3))
));
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
}
#[test]
fn test_search() {
let store: Arc<CredentialStore> = Store::new().unwrap();
let all = store.search(&HashMap::from([])).unwrap();
assert!(all.is_empty());
let all = store
.search(&HashMap::from([("service", ""), ("user", "")]))
.unwrap();
assert!(all.is_empty());
let e1 = store.build("foo", "bar", None).unwrap();
e1.set_password("e1").unwrap();
let all = store.search(&HashMap::from([])).unwrap();
assert_eq!(all.len(), 1);
let all = store
.search(&HashMap::from([("service", ""), ("user", "")]))
.unwrap();
assert_eq!(all.len(), 1);
let e2 = store.build("foo", "bam", None).unwrap();
e2.set_password("e2").unwrap();
let one = store.search(&HashMap::from([("user", "m")])).unwrap();
assert_eq!(one.len(), 1);
let one = store
.search(&HashMap::from([("service", "foo"), ("user", "bar")]))
.unwrap();
assert_eq!(one.len(), 1);
let two = store.search(&HashMap::from([("service", "foo")])).unwrap();
assert_eq!(two.len(), 2);
let all = store.search(&HashMap::from([("foo", "bar")])).unwrap();
assert_eq!(all.len(), 2);
}
#[test]
fn test_persistence() {
let store: Arc<CredentialStore> = Store::new().unwrap();
assert!(matches!(
store.persistence(),
CredentialPersistence::ProcessOnly
))
}
}