#![cfg_attr(docsrs, feature(doc_cfg))]
use std::collections::HashMap;
pub use credential::{Credential, CredentialBuilder};
pub use error::{Error, Result};
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod blocking;
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(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 = "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 = "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::LazyLock<Box<CredentialBuilder>> =
std::sync::LazyLock::new(default_credential_builder);
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);
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<Self> {
let entry = build_default_credential(None, service, user)?;
Ok(entry)
}
pub fn new_with_target(target: &str, service: &str, user: &str) -> Result<Self> {
let entry = build_default_credential(Some(target), service, user)?;
Ok(entry)
}
pub fn new_with_credential(credential: Box<Credential>) -> Self {
Self { inner: credential }
}
pub async fn set_password(&self, password: &str) -> Result<()> {
self.inner.set_password(password).await
}
pub async fn set_secret(&self, secret: &[u8]) -> Result<()> {
self.inner.set_secret(secret).await
}
pub async fn get_password(&self) -> Result<String> {
self.inner.get_password().await
}
pub async fn get_secret(&self) -> Result<Vec<u8>> {
self.inner.get_secret().await
}
pub async fn get_attributes(&self) -> Result<HashMap<String, String>> {
self.inner.get_attributes().await
}
pub async fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
self.inner.update_attributes(attributes).await
}
pub async fn delete_credential(&self) -> Result<()> {
self.inner.delete_credential().await
}
pub fn get_credential(&self) -> &dyn std::any::Any {
self.inner.as_any()
}
}
#[cfg(doctest)]
doc_comment::doctest!("../README.md", readme);
#[cfg(test)]
mod tests {
use super::{Entry, Error};
#[cfg(feature = "native-auth")]
use super::{Result, credential::CredentialApi};
use std::collections::HashMap;
#[cfg(feature = "native-auth")]
pub(crate) 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:?}")
}
}
}
async fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
entry
.set_password(in_pass)
.await
.unwrap_or_else(|err| panic!("Can't set password for {case}: {err:?}"));
let out_pass = entry
.get_password()
.await
.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(crate) async fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
test_round_trip_no_delete(case, entry, in_pass).await;
entry
.delete_credential()
.await
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
let password = entry.get_password().await;
assert!(
matches!(password, Err(Error::NoEntry)),
"Read deleted password for {case}",
);
}
pub(crate) async fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
entry
.set_secret(in_secret)
.await
.unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
let out_secret = entry
.get_secret()
.await
.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()
.await
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
let password = entry.get_secret().await;
assert!(
matches!(password, Err(Error::NoEntry)),
"Read deleted password for {case}",
);
}
pub(crate) fn generate_random_string_of_len(len: usize) -> String {
use fastrand;
use std::iter::repeat_with;
repeat_with(fastrand::alphanumeric).take(len).collect()
}
pub(crate) 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(crate) async 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().await, Err(Error::NoEntry)),
"Missing entry has password"
);
}
pub(crate) async 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, "").await;
}
pub(crate) async 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").await;
}
pub(crate) async 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, "このきれいな花は桜です").await;
}
pub(crate) async 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()).await;
}
pub(crate) async 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").await;
test_round_trip(
"updated non-ascii password",
&entry,
"このきれいな花は桜です",
)
.await;
}
pub(crate) async 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().await, 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).await, Err(Error::NoEntry)),
"Updated missing credential in attribute test",
);
entry
.set_password("test password for attributes")
.await
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
match entry.get_attributes().await {
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).await, Ok(())),
"Couldn't update attributes in attribute test",
);
match entry.get_attributes().await {
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()
.await
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
assert!(
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
"Read deleted credential in attribute test",
);
}
}