use std::collections::HashMap;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use security_framework::base::Error;
use security_framework::item;
use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
use security_framework::os::macos::passwords::find_generic_password;
use keyring_core::{
Entry,
api::{Credential, CredentialApi, CredentialPersistence, CredentialStoreApi},
attributes::parse_attributes,
error::{Error as ErrorCode, Result},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cred {
pub domain: MacKeychainDomain,
pub service: String,
pub account: String,
}
impl CredentialApi for Cred {
fn set_secret(&self, secret: &[u8]) -> Result<()> {
self.get_keychain()?
.set_generic_password(&self.service, &self.account, secret)
.map_err(decode_error)?;
Ok(())
}
fn get_secret(&self) -> Result<Vec<u8>> {
let (password_bytes, _) =
find_generic_password(Some(&[self.get_keychain()?]), &self.service, &self.account)
.map_err(decode_error)?;
Ok(password_bytes.to_owned())
}
fn delete_credential(&self) -> Result<()> {
let keychains = [self.get_keychain()?];
let mut options = item::ItemSearchOptions::new();
options
.keychains(&keychains)
.class(item::ItemClass::generic_password())
.service(&self.service)
.account(&self.account);
options.delete().map_err(decode_error)
}
fn get_credential(&self) -> Result<Option<Arc<Credential>>> {
find_generic_password(Some(&[self.get_keychain()?]), &self.service, &self.account)
.map_err(decode_error)?;
Ok(None)
}
fn get_specifiers(&self) -> Option<(String, String)> {
Some((self.service.clone(), self.account.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 build(keychain: MacKeychainDomain, service: &str, user: &str) -> Result<Entry> {
if service.is_empty() {
return Err(ErrorCode::Invalid(
"service".to_string(),
"cannot be empty".to_string(),
));
}
if user.is_empty() {
return Err(ErrorCode::Invalid(
"user".to_string(),
"cannot be empty".to_string(),
));
}
let cred = Cred {
domain: keychain,
service: service.to_string(),
account: user.to_string(),
};
Ok(Entry::new_with_credential(Arc::new(cred)))
}
fn get_keychain(&self) -> Result<SecKeychain> {
get_keychain(&self.domain)
}
}
pub struct Store {
id: String,
keychain: MacKeychainDomain,
}
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("domain", &self.keychain)
.finish()
}
}
impl Store {
pub fn new() -> Result<Arc<Self>> {
Ok(Self::new_internal(MacKeychainDomain::User))
}
pub fn new_with_configuration(configuration: &HashMap<&str, &str>) -> Result<Arc<Self>> {
let config = parse_attributes(&["keychain"], Some(configuration))?;
let mut keychain = MacKeychainDomain::User;
if let Some(option) = config.get("keychain") {
keychain = option.parse()?;
}
Ok(Self::new_internal(keychain))
}
fn new_internal(keychain: MacKeychainDomain) -> Arc<Self> {
let now = SystemTime::now();
let elapsed = if now.lt(&UNIX_EPOCH) {
UNIX_EPOCH.duration_since(now).unwrap()
} else {
now.duration_since(UNIX_EPOCH).unwrap()
};
Arc::new(Store {
id: format!(
"Keychain Storage, Crate version {}, Instantiated at {}",
env!("CARGO_PKG_VERSION"),
elapsed.as_secs_f64()
),
keychain,
})
}
}
impl CredentialStoreApi for Store {
fn vendor(&self) -> String {
"macOS Keychain Store, https://crates.io/crates/apple-native-keyring-store".to_string()
}
fn id(&self) -> String {
self.id.to_string()
}
fn build(
&self,
service: &str,
user: &str,
modifiers: Option<&HashMap<&str, &str>>,
) -> Result<Entry> {
let mods = parse_attributes(&["keychain"], modifiers)?;
let mut keychain = self.keychain.clone();
if let Some(option) = mods.get("keychain") {
keychain = option.parse()?;
}
Cred::build(keychain, service, user)
}
fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
let spec = parse_attributes(&["service", "user"], Some(spec))?;
let keychains = [get_keychain(&self.keychain)?];
let mut options = item::ItemSearchOptions::new();
options
.keychains(&keychains)
.class(item::ItemClass::generic_password())
.limit(item::Limit::All)
.load_attributes(true);
if let Some(service) = spec.get("service") {
options.service(service);
}
if let Some(user) = spec.get("user") {
options.account(user);
}
let items = match options.search().map_err(decode_error) {
Ok(items) => items,
Err(ErrorCode::NoEntry) => return Ok(Vec::new()),
Err(e) => return Err(e),
};
let mut result = Vec::new();
for item in items {
if let Some(map) = item.simplify_dict() {
if let Some(service) = map.get("svce") {
if let Some(account) = map.get("acct") {
let cred = Cred {
domain: self.keychain.clone(),
service: service.to_string(),
account: account.to_string(),
};
result.push(Entry::new_with_credential(Arc::new(cred)))
}
}
}
}
Ok(result)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn persistence(&self) -> CredentialPersistence {
CredentialPersistence::UntilDelete
}
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MacKeychainDomain {
User,
System,
Common,
Dynamic,
}
impl std::fmt::Display for MacKeychainDomain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MacKeychainDomain::User => "User".fmt(f),
MacKeychainDomain::System => "System".fmt(f),
MacKeychainDomain::Common => "Common".fmt(f),
MacKeychainDomain::Dynamic => "Dynamic".fmt(f),
}
}
}
impl std::str::FromStr for MacKeychainDomain {
type Err = ErrorCode;
fn from_str(s: &str) -> Result<Self> {
match s.to_ascii_lowercase().as_str() {
"user" => Ok(MacKeychainDomain::User),
"system" => Ok(MacKeychainDomain::System),
"common" => Ok(MacKeychainDomain::Common),
"dynamic" => Ok(MacKeychainDomain::Dynamic),
_ => Err(ErrorCode::Invalid(
"keychain".to_string(),
format!("'{s}' is not User, System, Common, or Dynamic"),
)),
}
}
}
fn get_keychain(domain: &MacKeychainDomain) -> Result<SecKeychain> {
let domain = match domain {
MacKeychainDomain::User => SecPreferencesDomain::User,
MacKeychainDomain::System => SecPreferencesDomain::System,
MacKeychainDomain::Common => SecPreferencesDomain::Common,
MacKeychainDomain::Dynamic => SecPreferencesDomain::Dynamic,
};
match SecKeychain::default_for_domain(domain) {
Ok(keychain) => Ok(keychain),
Err(err) => Err(decode_error(err)),
}
}
pub fn decode_error(err: Error) -> ErrorCode {
match err.code() {
-61 => ErrorCode::NoStorageAccess(Box::new(err)), -25244 => ErrorCode::NoStorageAccess(Box::new(err)), -25291 => ErrorCode::NoStorageAccess(Box::new(err)), -25292 => ErrorCode::NoStorageAccess(Box::new(err)), -25294 => ErrorCode::NoStorageAccess(Box::new(err)), -25295 => ErrorCode::NoStorageAccess(Box::new(err)), -25300 => ErrorCode::NoEntry, _ => ErrorCode::PlatformFailure(Box::new(err)),
}
}