use security_framework::item;
use std::collections::HashMap;
use super::error::{Error as ErrorCode, Result};
use super::search::{CredentialSearch, CredentialSearchApi, CredentialSearchResult};
pub struct MacCredentialSearch {}
pub fn default_credential_search() -> Box<CredentialSearch> {
Box::new(MacCredentialSearch {})
}
impl CredentialSearchApi for MacCredentialSearch {
fn by(&self, by: &str, query: &str) -> CredentialSearchResult {
search(by, query)
}
}
enum MacSearchType {
Label,
Service,
Account,
}
fn search(by: &str, query: &str) -> CredentialSearchResult {
let mut count = 0;
let mut new_search = item::ItemSearchOptions::new();
let search_default = &mut new_search
.class(item::ItemClass::generic_password())
.limit(item::Limit::All)
.load_attributes(true)
.case_insensitive(Some(true));
let by = match by.to_ascii_lowercase().as_str() {
"target" => MacSearchType::Label,
"service" => MacSearchType::Service,
"user" => MacSearchType::Account,
_ => {
return Err(ErrorCode::SearchError(
"Invalid search parameter, not Label, Service, or Account".to_string(),
))
}
};
let search = match by {
MacSearchType::Label => search_default.label(query).search(),
MacSearchType::Service => search_default.service(query).search(),
MacSearchType::Account => search_default.account(query).search(),
};
let mut outer_map: HashMap<String, HashMap<String, String>> = HashMap::new();
let results = match search {
Ok(items) => items,
Err(_) => return Err(ErrorCode::NoResults),
};
for item in results {
count += 1;
match to_credential_search_result(item.simplify_dict(), &mut outer_map, count) {
Ok(_) => {}
Err(err) => return Err(err),
}
}
Ok(outer_map)
}
fn to_credential_search_result(
item: Option<HashMap<String, String>>,
outer_map: &mut HashMap<String, HashMap<String, String>>,
count: u32,
) -> Result<()> {
let result = match item {
None => return Err(ErrorCode::NoResults),
Some(map) => map,
};
outer_map.insert(count.to_string(), result);
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{tests::generate_random_string, Error, Limit, List, Search};
use core_foundation::{
base::{CFGetTypeID, CFTypeRef, TCFType, TCFTypeRef},
date::{CFDate, CFDateRef},
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
number::{kCFBooleanTrue, CFNumber, CFNumberRef},
propertylist::CFPropertyListSubClass,
string::{CFString, CFStringRef},
};
use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
use security_framework_sys::{
base::errSecSuccess,
item::{kSecReturnAttributes, kSecValueRef},
};
fn get_keychain() -> SecKeychain {
SecKeychain::default_for_domain(SecPreferencesDomain::User)
.expect("Failed to get default keychain for User domain")
}
fn create_credential(name: &str, user: Option<&str>) {
let keychain = get_keychain();
let password = "test-password".as_bytes();
keychain
.set_generic_password(name, user.unwrap_or(name), password)
.expect("Error creating test credential");
}
fn delete_credential(name: &str, user: Option<&str>) {
let keychain = get_keychain();
let (_password, item) = keychain
.find_generic_password(name, user.unwrap_or(name))
.expect("Error getting test credential");
item.delete();
}
fn test_search(by: &str) {
let result_count = 1;
let name = generate_random_string();
create_credential(&name, None);
let search_result = match by.to_ascii_lowercase().as_str() {
"account" => Search::new()
.expect("Error creating mac search test")
.by_user(&name),
"service" => Search::new()
.expect("Error creating mac search test")
.by_service(&name),
"label" => Search::new()
.expect("Error creating mac search test")
.by_target(&name),
_ => panic!("unexpected search by parameter"),
};
let list_result = List::list_credentials(&search_result, Limit::All);
let keychain = get_keychain();
let mut expected = String::new();
let item = &keychain
.find_generic_password(&name, &name)
.expect("Error finding test credential")
.1;
let mut query: CFMutableDictionary<CFString, CFTypeRef> = CFMutableDictionary::new();
unsafe {
query.add(
&CFString::wrap_under_get_rule(kSecValueRef),
&item.as_CFTypeRef(),
);
query.add(
&CFString::wrap_under_get_rule(kSecReturnAttributes),
&kCFBooleanTrue.as_void_ptr(),
);
}
let mut result: CFTypeRef = std::ptr::null();
let status = unsafe {
security_framework_sys::keychain_item::SecItemCopyMatching(
query.as_concrete_TypeRef(),
&mut result as *mut _,
)
};
if status == errSecSuccess {
let attributes: CFDictionary =
unsafe { CFDictionary::wrap_under_create_rule(result as CFDictionaryRef) };
let count = attributes.len() as isize;
let mut keys: Vec<CFTypeRef> = Vec::with_capacity(count as usize);
let mut values: Vec<CFTypeRef> = Vec::with_capacity(count as usize);
unsafe {
keys.set_len(count as usize);
values.set_len(count as usize);
}
let (keys, values) = attributes.get_keys_and_values();
for (key, value) in keys.into_iter().zip(values.into_iter()) {
let key_str =
unsafe { CFString::wrap_under_get_rule(key as CFStringRef).to_string() };
let cfdate_id = CFDate::type_id();
let cfnumber_id = CFNumber::type_id();
let cfstring_id = CFString::type_id();
let value_str = match unsafe { CFGetTypeID(value) } {
id if id == cfdate_id => {
let new_str = format!("{:?}", unsafe {
CFDate::wrap_under_get_rule(value as CFDateRef).to_CFPropertyList()
});
new_str.trim_matches('"').to_string()
}
id if id == cfnumber_id => {
format!(
"{}",
unsafe { CFNumber::wrap_under_get_rule(value as CFNumberRef) }
.to_i32()
.unwrap()
)
}
id if id == cfstring_id => {
format!("{}", unsafe {
CFString::wrap_under_get_rule(value as CFStringRef)
})
}
_ => "Error getting type ID".to_string(),
};
if key_str == "crtr".to_string() {
expected.push_str(format!("{}: unknown\n", key_str).as_str());
} else {
expected.push_str(format!("{}: {}\n", key_str, value_str).as_str());
}
expected.push_str(format!("{}\n", &result_count.to_string()).as_str());
}
}
let actual_set: HashSet<&str> = list_result.lines().collect();
let expected_set: HashSet<&str> = expected.lines().collect();
assert_eq!(actual_set, expected_set);
delete_credential(&name, None);
}
#[test]
fn test_search_by_service() {
test_search("service")
}
#[test]
fn test_search_by_label() {
test_search("label")
}
#[test]
fn test_search_by_account() {
test_search("account")
}
#[test]
fn test_max_result() {
let name1 = generate_random_string();
let name2 = generate_random_string();
let name3 = generate_random_string();
let name4 = generate_random_string();
create_credential(&name1, Some("test-user"));
create_credential(&name2, Some("test-user"));
create_credential(&name3, Some("test-user"));
create_credential(&name4, Some("test-user"));
let search = Search::new()
.expect("Error creating test-max-result search")
.by_user("test-user");
let list = List::list_credentials(&search, Limit::Max(1));
let lines = list.lines().count();
assert_eq!(8, lines);
delete_credential(&name1, Some("test-user"));
delete_credential(&name2, Some("test-user"));
delete_credential(&name3, Some("test-user"));
delete_credential(&name4, Some("test-user"));
}
#[test]
fn no_results() {
let name = generate_random_string();
let result = Search::new()
.expect("Failed to build new search")
.by_user(&name);
assert!(
matches!(result.unwrap_err(), Error::NoResults),
"Returned an empty value"
);
}
}