use crate::os::macos::keychain::SecKeychain;
use crate::os::macos::keychain_item::SecKeychainItem;
use core_foundation::array::CFArray;
use core_foundation::base::TCFType;
pub use security_framework_sys::keychain::{SecAuthenticationType, SecProtocolType};
use security_framework_sys::keychain::{
SecKeychainAddGenericPassword, SecKeychainAddInternetPassword, SecKeychainFindGenericPassword,
SecKeychainFindInternetPassword,
};
use security_framework_sys::keychain_item::{
SecKeychainItemDelete, SecKeychainItemFreeContent, SecKeychainItemModifyAttributesAndData,
};
use std::fmt;
use std::fmt::Write;
use std::ops::Deref;
use std::ptr;
use std::slice;
use crate::base::Result;
use crate::cvt;
pub struct SecKeychainItemPassword {
data: *const u8,
data_len: usize,
}
impl fmt::Debug for SecKeychainItemPassword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for _ in 0..self.data_len {
f.write_char('•')?;
}
Ok(())
}
}
impl AsRef<[u8]> for SecKeychainItemPassword {
#[inline]
fn as_ref(&self) -> &[u8] {
unsafe { slice::from_raw_parts(self.data, self.data_len) }
}
}
impl Deref for SecKeychainItemPassword {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl Drop for SecKeychainItemPassword {
fn drop(&mut self) {
unsafe {
SecKeychainItemFreeContent(ptr::null_mut(), self.data as *mut _);
}
}
}
impl SecKeychainItem {
pub fn set_password(&mut self, password: &[u8]) -> Result<()> {
unsafe {
cvt(SecKeychainItemModifyAttributesAndData(
self.as_CFTypeRef() as *mut _,
ptr::null(),
password.len() as u32,
password.as_ptr() as *const _,
))?;
}
Ok(())
}
pub fn delete(self) {
unsafe {
SecKeychainItemDelete(self.as_CFTypeRef() as *mut _);
}
}
}
pub fn find_generic_password(
keychains: Option<&[SecKeychain]>,
service: &str,
account: &str,
) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
let keychains_or_none = keychains.map(|refs| CFArray::from_CFTypes(refs));
let keychains_or_null = match keychains_or_none {
None => ptr::null(),
Some(ref keychains) => keychains.as_CFTypeRef(),
};
let mut data_len = 0;
let mut data = ptr::null_mut();
let mut item = ptr::null_mut();
unsafe {
cvt(SecKeychainFindGenericPassword(
keychains_or_null,
service.len() as u32,
service.as_ptr() as *const _,
account.len() as u32,
account.as_ptr() as *const _,
&mut data_len,
&mut data,
&mut item,
))?;
Ok((
SecKeychainItemPassword {
data: data as *const _,
data_len: data_len as usize,
},
SecKeychainItem::wrap_under_create_rule(item),
))
}
}
pub fn find_internet_password(
keychains: Option<&[SecKeychain]>,
server: &str,
security_domain: Option<&str>,
account: &str,
path: &str,
port: Option<u16>,
protocol: SecProtocolType,
authentication_type: SecAuthenticationType,
) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
let keychains_or_none = keychains.map(|refs| CFArray::from_CFTypes(refs));
let keychains_or_null = match keychains_or_none {
None => ptr::null(),
Some(ref keychains) => keychains.as_CFTypeRef(),
};
let mut data_len = 0;
let mut data = ptr::null_mut();
let mut item = ptr::null_mut();
unsafe {
cvt(SecKeychainFindInternetPassword(
keychains_or_null,
server.len() as u32,
server.as_ptr() as *const _,
security_domain.map_or(0, |s| s.len() as u32),
security_domain
.map_or(ptr::null(), |s| s.as_ptr() as *const _),
account.len() as u32,
account.as_ptr() as *const _,
path.len() as u32,
path.as_ptr() as *const _,
port.unwrap_or(0),
protocol,
authentication_type,
&mut data_len,
&mut data,
&mut item,
))?;
Ok((
SecKeychainItemPassword {
data: data as *const _,
data_len: data_len as usize,
},
SecKeychainItem::wrap_under_create_rule(item),
))
}
}
impl SecKeychain {
pub fn find_generic_password(
&self,
service: &str,
account: &str,
) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
find_generic_password(Some(&[self.clone()]), service, account)
}
pub fn find_internet_password(
&self,
server: &str,
security_domain: Option<&str>,
account: &str,
path: &str,
port: Option<u16>,
protocol: SecProtocolType,
authentication_type: SecAuthenticationType,
) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
find_internet_password(
Some(&[self.clone()]),
server,
security_domain,
account,
path,
port,
protocol,
authentication_type,
)
}
pub fn set_internet_password(
&self,
server: &str,
security_domain: Option<&str>,
account: &str,
path: &str,
port: Option<u16>,
protocol: SecProtocolType,
authentication_type: SecAuthenticationType,
password: &[u8],
) -> Result<()> {
match self.find_internet_password(
server,
security_domain,
account,
path,
port,
protocol,
authentication_type,
) {
Ok((_, mut item)) => item.set_password(password),
_ => self.add_internet_password(
server,
security_domain,
account,
path,
port,
protocol,
authentication_type,
password,
),
}
}
pub fn set_generic_password(
&self,
service: &str,
account: &str,
password: &[u8],
) -> Result<()> {
match self.find_generic_password(service, account) {
Ok((_, mut item)) => item.set_password(password),
_ => self.add_generic_password(service, account, password),
}
}
pub fn add_generic_password(
&self,
service: &str,
account: &str,
password: &[u8],
) -> Result<()> {
unsafe {
cvt(SecKeychainAddGenericPassword(
self.as_CFTypeRef() as *mut _,
service.len() as u32,
service.as_ptr() as *const _,
account.len() as u32,
account.as_ptr() as *const _,
password.len() as u32,
password.as_ptr() as *const _,
ptr::null_mut(),
))?;
}
Ok(())
}
pub fn add_internet_password(
&self,
server: &str,
security_domain: Option<&str>,
account: &str,
path: &str,
port: Option<u16>,
protocol: SecProtocolType,
authentication_type: SecAuthenticationType,
password: &[u8],
) -> Result<()> {
unsafe {
cvt(SecKeychainAddInternetPassword(
self.as_CFTypeRef() as *mut _,
server.len() as u32,
server.as_ptr() as *const _,
security_domain.map_or(0, |s| s.len() as u32),
security_domain
.map_or(ptr::null(), |s| s.as_ptr() as *const _),
account.len() as u32,
account.as_ptr() as *const _,
path.len() as u32,
path.as_ptr() as *const _,
port.unwrap_or(0),
protocol,
authentication_type,
password.len() as u32,
password.as_ptr() as *const _,
ptr::null_mut(),
))?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::os::macos::keychain::{CreateOptions, SecKeychain};
use tempdir::TempDir;
fn temp_keychain_setup(name: &str) -> (TempDir, SecKeychain) {
let dir = TempDir::new("passwords").expect("TempDir::new");
let keychain = CreateOptions::new()
.password("foobar")
.create(dir.path().join(name.to_string() + ".keychain"))
.expect("create keychain");
(dir, keychain)
}
fn temp_keychain_teardown(dir: TempDir) {
dir.close().expect("temp dir close");
}
#[test]
fn missing_password_temp() {
let (dir, keychain) = temp_keychain_setup("missing_password");
let keychains = vec![keychain];
let service = "temp_this_service_does_not_exist";
let account = "this_account_is_bogus";
let found = find_generic_password(Some(&keychains), service, account);
assert!(found.is_err());
temp_keychain_teardown(dir);
}
#[test]
#[cfg(feature = "default_keychain_tests")]
fn missing_password_default() {
let service = "default_this_service_does_not_exist";
let account = "this_account_is_bogus";
let found = find_generic_password(None, service, account);
assert!(found.is_err());
}
#[test]
fn round_trip_password_temp() {
let (dir, keychain) = temp_keychain_setup("round_trip_password");
let service = "test_round_trip_password_temp";
let account = "temp_this_is_the_test_account";
let password = String::from("deadbeef").into_bytes();
keychain
.set_generic_password(service, account, &password)
.expect("set_generic_password");
let (found, item) = keychain
.find_generic_password(service, account)
.expect("find_generic_password");
assert_eq!(found.to_owned(), password);
item.delete();
temp_keychain_teardown(dir);
}
#[test]
#[cfg(feature = "default_keychain_tests")]
fn round_trip_password_default() {
let service = "test_round_trip_password_default";
let account = "this_is_the_test_account";
let password = String::from("deadbeef").into_bytes();
SecKeychain::default()
.expect("default keychain")
.set_generic_password(service, account, &password)
.expect("set_generic_password");
let (found, item) =
find_generic_password(None, service, account).expect("find_generic_password");
assert_eq!(&*found, &password[..]);
item.delete();
}
#[test]
fn change_password_temp() {
let (dir, keychain) = temp_keychain_setup("change_password");
let keychains = vec![keychain];
let service = "test_change_password_temp";
let account = "this_is_the_test_account";
let pw1 = String::from("password1").into_bytes();
let pw2 = String::from("password2").into_bytes();
keychains[0]
.set_generic_password(service, account, &pw1)
.expect("set_generic_password1");
let (found, _) = find_generic_password(Some(&keychains), service, account)
.expect("find_generic_password1");
assert_eq!(found.as_ref(), &pw1[..]);
keychains[0]
.set_generic_password(service, account, &pw2)
.expect("set_generic_password2");
let (found, item) = find_generic_password(Some(&keychains), service, account)
.expect("find_generic_password2");
assert_eq!(&*found, &pw2[..]);
item.delete();
temp_keychain_teardown(dir);
}
#[test]
#[cfg(feature = "default_keychain_tests")]
fn change_password_default() {
let service = "test_change_password_default";
let account = "this_is_the_test_account";
let pw1 = String::from("password1").into_bytes();
let pw2 = String::from("password2").into_bytes();
SecKeychain::default()
.expect("default keychain")
.set_generic_password(service, account, &pw1)
.expect("set_generic_password1");
let (found, _) =
find_generic_password(None, service, account).expect("find_generic_password1");
assert_eq!(found.to_owned(), pw1);
SecKeychain::default()
.expect("default keychain")
.set_generic_password(service, account, &pw2)
.expect("set_generic_password2");
let (found, item) =
find_generic_password(None, service, account).expect("find_generic_password2");
assert_eq!(found.to_owned(), pw2);
item.delete();
}
#[test]
fn cross_keychain_corruption_temp() {
let (dir1, keychain1) = temp_keychain_setup("cross_corrupt1");
let (dir2, keychain2) = temp_keychain_setup("cross_corrupt2");
let keychains1 = vec![keychain1.clone()];
let keychains2 = vec![keychain2.clone()];
let both_keychains = vec![keychain1, keychain2];
let service = "temp_this_service_does_not_exist";
let account = "this_account_is_bogus";
let password = String::from("deadbeef").into_bytes();
let found = find_generic_password(Some(&both_keychains), service, account);
assert!(found.is_err());
keychains1[0]
.set_generic_password(service, account, &password)
.expect("set_generic_password");
let (found, item) = find_generic_password(Some(&keychains1), service, account)
.expect("find_generic_password1");
assert_eq!(found.to_owned(), password);
let found = find_generic_password(Some(&keychains2), service, account);
assert!(found.is_err());
item.delete();
temp_keychain_teardown(dir1);
temp_keychain_teardown(dir2);
}
}