pub use credential::{Credential, CredentialBuilder};
pub use error::{Error, Result};
use std::collections::HashMap;
pub mod mock;
#[cfg(all(
not(doc),
any(
all(feature = "linux-native", feature = "sync-secret-service"),
all(feature = "linux-native", feature = "async-secret-service"),
all(feature = "sync-secret-service", feature = "async-secret-service")
)
))]
compile_error!("You can enable at most one keystore per target architecture");
#[cfg(all(target_os = "linux", feature = "linux-native"))]
pub mod keyutils;
#[cfg(all(
target_os = "linux",
feature = "linux-native",
not(all(
doc,
any(feature = "sync-secret-service", feature = "async-secret-service")
))
))]
pub use keyutils as default;
#[cfg(all(
any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
any(feature = "sync-secret-service", feature = "async-secret-service")
))]
pub mod secret_service;
#[cfg(all(
any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
any(feature = "sync-secret-service", feature = "async-secret-service")
))]
pub use secret_service as default;
#[cfg(all(
target_os = "linux",
not(any(
feature = "linux-native",
feature = "sync-secret-service",
feature = "async-secret-service"
))
))]
pub use mock as default;
#[cfg(all(
any(target_os = "freebsd", target_os = "openbsd"),
not(any(feature = "sync-secret-service", feature = "async-secret-service"))
))]
pub use mock as default;
#[cfg(all(target_os = "macos", feature = "apple-native"))]
pub mod macos;
#[cfg(all(target_os = "macos", feature = "apple-native"))]
pub use macos as default;
#[cfg(all(target_os = "macos", not(feature = "apple-native")))]
pub use mock as default;
#[cfg(all(target_os = "ios", feature = "apple-native"))]
pub mod ios;
#[cfg(all(target_os = "ios", feature = "apple-native"))]
pub use ios as default;
#[cfg(all(target_os = "ios", not(feature = "apple-native")))]
pub use mock as default;
#[cfg(all(target_os = "windows", feature = "windows-native"))]
pub mod windows;
#[cfg(all(target_os = "windows", not(feature = "windows-native")))]
pub use mock as default;
#[cfg(all(target_os = "windows", feature = "windows-native"))]
pub use windows as default;
#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "macos",
target_os = "ios",
target_os = "windows",
)))]
pub use mock as default;
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);
}
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::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> {
build_default_credential(None, service, user)
}
pub fn new_with_target(target: &str, service: &str, user: &str) -> Result<Entry> {
build_default_credential(Some(target), service, user)
}
pub fn new_with_credential(credential: Box<Credential>) -> Entry {
Entry { inner: credential }
}
pub fn set_password(&self, password: &str) -> Result<()> {
self.inner.set_password(password)
}
pub fn set_secret(&self, secret: &[u8]) -> Result<()> {
self.inner.set_secret(secret)
}
pub fn get_password(&self) -> Result<String> {
self.inner.get_password()
}
pub fn get_secret(&self) -> Result<Vec<u8>> {
self.inner.get_secret()
}
pub fn get_attributes(&self) -> Result<HashMap<String, String>> {
self.inner.get_attributes()
}
pub fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
self.inner.update_attributes(attributes)
}
pub fn delete_credential(&self) -> Result<()> {
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::{credential::CredentialApi, Entry, Error, Result};
use rand::Rng;
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:?}")
}
}
}
pub fn test_round_trip(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}'",
);
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 rand::{distributions::Alphanumeric, thread_rng, Rng};
thread_rng()
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
.collect()
}
pub fn generate_random_string() -> String {
generate_random_string_of_len(30)
}
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 mut secret: [u8; 16] = [0; 16];
rand::rngs::OsRng.fill(&mut secret);
test_round_trip_secret("non-ascii password", &entry, &secret);
}
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("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",
);
}
}