#![cfg_attr(docsrs, feature(doc_cfg))]
use log::debug;
use std::collections::HashMap;
pub use credential::{Credential, CredentialBuilder};
pub use error::{Error, Result};
pub mod mock;
#[cfg(all(
any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
feature = "secret-service"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd")))
)]
pub mod secret_service;
#[cfg(all(target_os = "macos", feature = "apple-native"))]
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
pub mod macos;
#[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "apple-native"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))]
pub mod ios;
#[cfg(all(target_os = "windows", feature = "windows-native"))]
#[cfg_attr(docsrs, doc(cfg(target_os = "windows")))]
pub mod windows;
pub mod credential;
pub mod error;
#[derive(Default, Debug)]
struct EntryBuilder {
inner: Option<Box<CredentialBuilder>>,
}
static DEFAULT_BUILDER: std::sync::RwLock<EntryBuilder> =
std::sync::RwLock::new(EntryBuilder { inner: None });
pub fn set_default_credential_builder(new: Box<CredentialBuilder>) {
let mut guard = DEFAULT_BUILDER
.write()
.expect("Poisoned RwLock in keyring-rs: please report a bug!");
guard.inner = Some(new);
}
pub fn default_credential_builder() -> Box<CredentialBuilder> {
#[cfg(any(
all(target_os = "linux", feature = "secret-service"),
all(target_os = "freebsd", feature = "secret-service"),
all(target_os = "openbsd", feature = "secret-service")
))]
return secret_service::default_credential_builder();
#[cfg(all(target_os = "macos", feature = "apple-native"))]
return macos::default_credential_builder();
#[cfg(all(target_os = "ios", feature = "apple-native"))]
return ios::default_credential_builder();
#[cfg(all(target_os = "windows", feature = "windows-native"))]
return windows::default_credential_builder();
#[cfg(not(any(
all(target_os = "linux", feature = "secret-service"),
all(target_os = "freebsd", feature = "secret-service"),
all(target_os = "openbsd", feature = "secret-service"),
all(target_os = "macos", feature = "apple-native"),
all(target_os = "ios", feature = "apple-native"),
all(target_os = "windows", feature = "windows-native"),
)))]
credential::nop_credential_builder()
}
fn build_default_credential(target: Option<&str>, service: &str, user: &str) -> Result<Entry> {
static DEFAULT: std::sync::OnceLock<Box<CredentialBuilder>> = std::sync::OnceLock::new();
let guard = DEFAULT_BUILDER
.read()
.expect("Poisoned RwLock in keyring-rs: please report a bug!");
let builder = guard
.inner
.as_ref()
.unwrap_or_else(|| DEFAULT.get_or_init(|| default_credential_builder()));
let credential = builder.build(target, service, user)?;
Ok(Entry { inner: credential })
}
#[derive(Debug)]
pub struct Entry {
inner: Box<Credential>,
}
impl Entry {
pub fn new(service: &str, user: &str) -> Result<Entry> {
debug!("creating entry with service {service}, user {user}, and no target");
let entry = build_default_credential(None, service, user)?;
debug!("created entry {:?}", entry.inner);
Ok(entry)
}
pub fn new_with_target(target: &str, service: &str, user: &str) -> Result<Entry> {
debug!("creating entry with service {service}, user {user}, and target {target}");
let entry = build_default_credential(Some(target), service, user)?;
debug!("created entry {:?}", entry.inner);
Ok(entry)
}
pub fn new_with_credential(credential: Box<Credential>) -> Entry {
debug!("create entry from {credential:?}");
Entry { inner: credential }
}
pub fn set_password(&self, password: &str) -> Result<()> {
debug!("set password for entry {:?}", self.inner);
self.inner.set_password(password)
}
pub fn set_secret(&self, secret: &[u8]) -> Result<()> {
debug!("set secret for entry {:?}", self.inner);
self.inner.set_secret(secret)
}
pub fn get_password(&self) -> Result<String> {
debug!("get password from entry {:?}", self.inner);
self.inner.get_password()
}
pub fn get_secret(&self) -> Result<Vec<u8>> {
debug!("get secret from entry {:?}", self.inner);
self.inner.get_secret()
}
pub fn get_attributes(&self) -> Result<HashMap<String, String>> {
debug!("get attributes from entry {:?}", self.inner);
self.inner.get_attributes()
}
pub fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
debug!(
"update attributes for entry {:?} from map {attributes:?}",
self.inner
);
self.inner.update_attributes(attributes)
}
pub fn delete_credential(&self) -> Result<()> {
debug!("delete entry {:?}", self.inner);
self.inner.delete_credential()
}
pub fn get_credential(&self) -> &dyn std::any::Any {
self.inner.as_any()
}
}
#[cfg(doctest)]
doc_comment::doctest!("../README.md", readme);
#[cfg(test)]
#[allow(dead_code)]
mod tests {
use super::{Entry, Error, Result, credential::CredentialApi};
use std::collections::HashMap;
pub fn entry_from_constructor<F, T>(f: F, service: &str, user: &str) -> Entry
where
F: FnOnce(Option<&str>, &str, &str) -> Result<T>,
T: 'static + CredentialApi + Send + Sync,
{
match f(None, service, user) {
Ok(credential) => Entry::new_with_credential(Box::new(credential)),
Err(err) => {
panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
}
}
}
pub fn entry_from_constructor_and_attributes<F, T>(
f: F,
service: &str,
user: &str,
attrs: &HashMap<&str, &str>,
) -> Entry
where
F: FnOnce(Option<&str>, &str, &str, &HashMap<&str, &str>) -> Result<T>,
T: 'static + CredentialApi + Send + Sync,
{
match f(None, service, user, attrs) {
Ok(credential) => Entry::new_with_credential(Box::new(credential)),
Err(err) => {
panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
}
}
}
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 for {case}: {err:?}"));
let out_pass = entry
.get_password()
.unwrap_or_else(|err| panic!("Can't get password for {case}: {err:?}"));
assert_eq!(
in_pass, out_pass,
"Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
)
}
pub 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 for {case}: {err:?}"));
let password = entry.get_password();
assert!(
matches!(password, Err(Error::NoEntry)),
"Read deleted password for {case}",
);
}
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,
"Passwords don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
);
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
let password = entry.get_secret();
assert!(
matches!(password, Err(Error::NoEntry)),
"Read deleted password for {case}",
);
}
pub fn generate_random_string_of_len(len: usize) -> String {
use fastrand;
use std::iter::repeat_with;
repeat_with(fastrand::alphanumeric).take(len).collect()
}
pub fn generate_random_string() -> String {
generate_random_string_of_len(30)
}
fn generate_random_bytes_of_len(len: usize) -> Vec<u8> {
use fastrand;
use std::iter::repeat_with;
repeat_with(|| fastrand::u8(..)).take(len).collect()
}
pub fn test_empty_service_and_user<F>(f: F)
where
F: Fn(&str, &str) -> Entry,
{
let name = generate_random_string();
let in_pass = "doesn't matter";
test_round_trip("empty user", &f(&name, ""), in_pass);
test_round_trip("empty service", &f("", &name), in_pass);
test_round_trip("empty service & user", &f("", ""), in_pass);
}
pub fn test_missing_entry<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Missing entry has password"
)
}
pub fn test_empty_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("empty password", &entry, "");
}
pub fn test_round_trip_ascii_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("ascii password", &entry, "test ascii password");
}
pub fn test_round_trip_non_ascii_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
}
pub fn test_round_trip_random_secret<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
let secret = generate_random_bytes_of_len(24);
test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
}
pub fn test_update<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
test_round_trip(
"updated non-ascii password",
&entry,
"このきれいな花は桜です",
);
}
pub fn test_noop_get_update_attributes<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
"Read missing credential in attribute test",
);
let mut map: HashMap<&str, &str> = HashMap::new();
map.insert("test attribute name", "test attribute value");
assert!(
matches!(entry.update_attributes(&map), Err(Error::NoEntry)),
"Updated missing credential in attribute test",
);
entry
.set_password("test password for attributes")
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
match entry.get_attributes() {
Err(err) => panic!("Couldn't get attributes: {err:?}"),
Ok(attrs) if attrs.is_empty() => {}
Ok(attrs) => panic!("Unexpected attributes: {attrs:?}"),
}
assert!(
matches!(entry.update_attributes(&map), Ok(())),
"Couldn't update attributes in attribute test",
);
match entry.get_attributes() {
Err(err) => panic!("Couldn't get attributes after update: {err:?}"),
Ok(attrs) if attrs.is_empty() => {}
Ok(attrs) => panic!("Unexpected attributes after update: {attrs:?}"),
}
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
"Read deleted credential in attribute test",
);
}
}