use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
use crate::error::{Error as ErrorCode, Result, decode_password};
use security_framework::base::Error;
use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
use security_framework::os::macos::passwords::find_generic_password;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MacCredential {
pub domain: MacKeychainDomain,
pub service: String,
pub account: String,
}
#[async_trait::async_trait]
impl CredentialApi for MacCredential {
async fn set_password(&self, password: &str) -> Result<()> {
let service = self.service.clone();
let account = self.account.clone();
let domain = self.domain;
let password = password.to_string();
crate::blocking::spawn_blocking(move || {
get_keychain(domain)?
.set_generic_password(&service, &account, password.as_bytes())
.map_err(decode_error)
})
.await?;
Ok(())
}
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
let service = self.service.clone();
let account = self.account.clone();
let domain = self.domain;
let secret = secret.to_vec();
crate::blocking::spawn_blocking(move || {
get_keychain(domain)?
.set_generic_password(&service, &account, &secret)
.map_err(decode_error)
})
.await?;
Ok(())
}
async fn get_password(&self) -> Result<String> {
let service = self.service.clone();
let account = self.account.clone();
let domain = self.domain;
let password_bytes = crate::blocking::spawn_blocking(move || -> Result<Vec<u8>> {
let keychain = get_keychain(domain)?;
let (password_bytes, _) = find_generic_password(Some(&[keychain]), &service, &account)
.map_err(decode_error)?;
Ok(password_bytes.to_owned())
})
.await?;
decode_password(password_bytes)
}
async fn get_secret(&self) -> Result<Vec<u8>> {
let service = self.service.clone();
let account = self.account.clone();
let domain = self.domain;
let password_bytes = crate::blocking::spawn_blocking(move || -> Result<Vec<u8>> {
let keychain = get_keychain(domain)?;
let (password_bytes, _) = find_generic_password(Some(&[keychain]), &service, &account)
.map_err(decode_error)?;
Ok(password_bytes.to_owned())
})
.await?;
Ok(password_bytes)
}
async fn delete_credential(&self) -> Result<()> {
let service = self.service.clone();
let account = self.account.clone();
let domain = self.domain;
crate::blocking::spawn_blocking(move || {
let keychain = get_keychain(domain)?;
let (_, item) = find_generic_password(Some(&[keychain]), &service, &account)
.map_err(decode_error)?;
item.delete();
Ok(())
})
.await?;
Ok(())
}
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 MacCredential {
pub async fn get_credential(&self) -> Result<Self> {
let service = self.service.clone();
let account = self.account.clone();
let domain = self.domain;
let keychain = get_keychain(domain)?;
crate::blocking::spawn_blocking(move || -> Result<()> {
let (_, _) = find_generic_password(Some(&[keychain]), &service, &account)
.map_err(decode_error)?;
Ok(())
})
.await?;
Ok(self.clone())
}
pub fn new_with_target(
target: Option<MacKeychainDomain>,
service: &str,
user: &str,
) -> Result<Self> {
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 domain = if let Some(target) = target {
target
} else {
MacKeychainDomain::User
};
Ok(Self {
domain,
service: service.to_string(),
account: user.to_string(),
})
}
}
pub struct MacCredentialBuilder;
pub fn default_credential_builder() -> Box<CredentialBuilder> {
Box::new(MacCredentialBuilder {})
}
impl CredentialBuilderApi for MacCredentialBuilder {
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
let domain: MacKeychainDomain = if let Some(target) = target {
target.parse().unwrap_or(MacKeychainDomain::User)
} else {
MacKeychainDomain::User
};
Ok(Box::new(MacCredential::new_with_target(
Some(domain),
service,
user,
)?))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum MacKeychainDomain {
User,
System,
Common,
Dynamic,
Protected,
}
impl std::fmt::Display for MacKeychainDomain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::User => "User".fmt(f),
Self::System => "System".fmt(f),
Self::Common => "Common".fmt(f),
Self::Dynamic => "Dynamic".fmt(f),
Self::Protected => "Protected".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(Self::User),
"system" => Ok(Self::System),
"common" => Ok(Self::Common),
"dynamic" => Ok(Self::Dynamic),
"protected" => Ok(Self::Protected),
"data protection" => Ok(Self::Protected),
_ => Err(ErrorCode::Invalid(
"target".to_string(),
format!("'{s}' is not User, System, Common, Dynamic, or Protected"),
)),
}
}
}
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,
MacKeychainDomain::Protected => panic!("Protected is not a keychain domain on macOS"),
};
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() {
-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)),
}
}
#[cfg(feature = "native-auth")]
#[cfg(not(miri))]
#[cfg(test)]
mod tests {
use crate::credential::CredentialPersistence;
use crate::{Entry, Error, tests::generate_random_string};
use super::{MacCredential, default_credential_builder};
#[test]
fn test_persistence() {
assert!(matches!(
default_credential_builder().persistence(),
CredentialPersistence::UntilDelete
));
}
fn entry_new(service: &str, user: &str) -> Entry {
crate::tests::entry_from_constructor(
|_, s, u| MacCredential::new_with_target(None, s, u),
service,
user,
)
}
#[test]
fn test_invalid_parameter() {
let credential = MacCredential::new_with_target(None, "", "user");
assert!(
matches!(credential, Err(Error::Invalid(_, _))),
"Created credential with empty service"
);
let credential = MacCredential::new_with_target(None, "service", "");
assert!(
matches!(credential, Err(Error::Invalid(_, _))),
"Created entry with empty user"
);
}
#[tokio::test]
async fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new).await;
}
#[tokio::test]
async fn test_empty_password() {
crate::tests::test_empty_password(entry_new).await;
}
#[tokio::test]
async fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new).await;
}
#[tokio::test]
async fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
}
#[tokio::test]
async fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new).await;
}
#[tokio::test]
async fn test_update() {
crate::tests::test_update(entry_new).await;
}
#[tokio::test]
async fn test_get_credential() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let credential: &MacCredential = entry
.get_credential()
.downcast_ref()
.expect("Not a mac credential");
assert!(
credential.get_credential().await.is_err(),
"Platform credential shouldn't exist yet!"
);
entry
.set_password("test get_credential")
.await
.expect("Can't set password for get_credential");
assert!(credential.get_credential().await.is_ok());
entry
.delete_credential()
.await
.expect("Couldn't delete after get_credential");
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)));
}
#[tokio::test]
async fn test_get_update_attributes() {
crate::tests::test_noop_get_update_attributes(entry_new).await;
}
#[test]
fn test_select_keychain() {
for name in ["unknown", "user", "common", "system", "dynamic"] {
let cred = Entry::new_with_target(name, name, name)
.expect("couldn't create credential")
.inner;
let mac_cred: &MacCredential = cred
.as_any()
.downcast_ref()
.expect("credential not a MacCredential");
if name == "unknown" {
assert!(
matches!(mac_cred.domain, super::MacKeychainDomain::User),
"wrong domain for unknown specifier"
);
}
}
}
}