//! Support to search for items in a keychain. use core_foundation::array::CFArray; use core_foundation::base::{CFType, TCFType, ToVoid}; use core_foundation::boolean::CFBoolean; use core_foundation::data::CFData; use core_foundation::date::CFDate; use core_foundation::dictionary::{CFDictionary, CFMutableDictionary}; use core_foundation::number::CFNumber; use core_foundation::string::CFString; use core_foundation_sys::base::{CFCopyDescription, CFGetTypeID, CFRelease, CFTypeRef}; use core_foundation_sys::string::CFStringRef; use security_framework_sys::item::*; use security_framework_sys::keychain_item::{SecItemAdd, SecItemCopyMatching}; use std::collections::HashMap; use std::fmt; use std::ptr; use crate::base::Result; use crate::certificate::SecCertificate; use crate::cvt; use crate::identity::SecIdentity; use crate::key::SecKey; #[cfg(target_os = "macos")] use crate::os::macos::keychain::SecKeychain; /// Specifies the type of items to search for. #[derive(Debug, Copy, Clone)] pub struct ItemClass(CFStringRef); impl ItemClass { /// Look for `SecKeychainItem`s corresponding to generic passwords. #[inline(always)] #[must_use] pub fn generic_password() -> Self { unsafe { Self(kSecClassGenericPassword) } } /// Look for `SecKeychainItem`s corresponding to internet passwords. #[inline(always)] #[must_use] pub fn internet_password() -> Self { unsafe { Self(kSecClassInternetPassword) } } /// Look for `SecCertificate`s. #[inline(always)] #[must_use] pub fn certificate() -> Self { unsafe { Self(kSecClassCertificate) } } /// Look for `SecKey`s. #[inline(always)] #[must_use] pub fn key() -> Self { unsafe { Self(kSecClassKey) } } /// Look for `SecIdentity`s. #[inline(always)] #[must_use] pub fn identity() -> Self { unsafe { Self(kSecClassIdentity) } } #[inline] fn to_value(self) -> CFType { unsafe { CFType::wrap_under_get_rule(self.0.cast()) } } } /// Specifies the type of keys to search for. #[derive(Debug, Copy, Clone)] pub struct KeyClass(CFStringRef); impl KeyClass { /// `kSecAttrKeyClassPublic` #[inline(always)] #[must_use] pub fn public() -> Self { unsafe { Self(kSecAttrKeyClassPublic) } } /// `kSecAttrKeyClassPrivate` #[inline(always)] #[must_use] pub fn private() -> Self { unsafe { Self(kSecAttrKeyClassPrivate) } } /// `kSecAttrKeyClassSymmetric` #[inline(always)] #[must_use] pub fn symmetric() -> Self { unsafe { Self(kSecAttrKeyClassSymmetric) } } #[inline] fn to_value(self) -> CFType { unsafe { CFType::wrap_under_get_rule(self.0.cast()) } } } /// Specifies the number of results returned by a search #[derive(Debug, Copy, Clone)] pub enum Limit { /// Always return all results All, /// Return up to the specified number of results Max(i64), } impl Limit { #[inline] fn to_value(self) -> CFType { match self { Self::All => unsafe { CFString::wrap_under_get_rule(kSecMatchLimitAll).into_CFType() }, Self::Max(l) => CFNumber::from(l).into_CFType(), } } } impl From for Limit { #[inline] fn from(limit: i64) -> Self { Self::Max(limit) } } /// A builder type to search for items in keychains. #[derive(Default)] pub struct ItemSearchOptions { #[cfg(target_os = "macos")] keychains: Option>, #[cfg(not(target_os = "macos"))] keychains: Option>, class: Option, key_class: Option, load_refs: bool, load_attributes: bool, load_data: bool, limit: Option, label: Option, service: Option, account: Option, access_group: Option, pub_key_hash: Option, app_label: Option, } #[cfg(target_os = "macos")] impl crate::ItemSearchOptionsInternals for ItemSearchOptions { #[inline] fn keychains(&mut self, keychains: &[SecKeychain]) -> &mut Self { self.keychains = Some(CFArray::from_CFTypes(keychains)); self } } impl ItemSearchOptions { /// Creates a new builder with default options. #[inline(always)] #[must_use] pub fn new() -> Self { Self::default() } /// Search only for items of the specified class. #[inline(always)] pub fn class(&mut self, class: ItemClass) -> &mut Self { self.class = Some(class); self } /// Search only for keys of the specified class. Also sets self.class to /// ItemClass::key(). #[inline(always)] pub fn key_class(&mut self, key_class: KeyClass) -> &mut Self { self.class(ItemClass::key()); self.key_class = Some(key_class); self } /// Load Security Framework objects (`SecCertificate`, `SecKey`, etc) for /// the results. #[inline(always)] pub fn load_refs(&mut self, load_refs: bool) -> &mut Self { self.load_refs = load_refs; self } /// Load Security Framework object attributes for /// the results. #[inline(always)] pub fn load_attributes(&mut self, load_attributes: bool) -> &mut Self { self.load_attributes = load_attributes; self } /// Load Security Framework objects data for /// the results. #[inline(always)] pub fn load_data(&mut self, load_data: bool) -> &mut Self { self.load_data = load_data; self } /// Limit the number of search results. /// /// If this is not called, the default limit is 1. #[inline(always)] pub fn limit>(&mut self, limit: T) -> &mut Self { self.limit = Some(limit.into()); self } /// Search for an item with the given label. #[inline(always)] pub fn label(&mut self, label: &str) -> &mut Self { self.label = Some(CFString::new(label)); self } /// Search for an item with the given service. #[inline(always)] pub fn service(&mut self, service: &str) -> &mut Self { self.service = Some(CFString::new(service)); self } /// Search for an item with the given account. #[inline(always)] pub fn account(&mut self, account: &str) -> &mut Self { self.account = Some(CFString::new(account)); self } /// Sets `kSecAttrAccessGroup` to `kSecAttrAccessGroupToken` #[inline(always)] pub fn access_group_token(&mut self) -> &mut Self { self.access_group = unsafe { Some(CFString::wrap_under_get_rule(kSecAttrAccessGroupToken)) }; self } /// Search for a certificate with the given public key hash. /// /// This is only compatible with [`ItemClass::certificate`], to search for /// a key by public key hash use [`ItemSearchOptions::application_label`] /// instead. #[inline(always)] pub fn pub_key_hash(&mut self, pub_key_hash: &[u8]) -> &mut Self { self.pub_key_hash = Some(CFData::from_buffer(pub_key_hash)); self } /// Search for a key with the given public key hash. /// /// This is only compatible with [`ItemClass::key`], to search for a /// certificate by the public key hash use [`ItemSearchOptions::pub_key_hash`] /// instead. #[inline(always)] pub fn application_label(&mut self, app_label: &[u8]) -> &mut Self { self.app_label = Some(CFData::from_buffer(app_label)); self } /// Search for objects. pub fn search(&self) -> Result> { unsafe { let mut params = vec![]; if let Some(ref keychains) = self.keychains { params.push(( CFString::wrap_under_get_rule(kSecMatchSearchList), keychains.as_CFType(), )); } if let Some(class) = self.class { params.push((CFString::wrap_under_get_rule(kSecClass), class.to_value())); } if let Some(key_class) = self.key_class { params.push((CFString::wrap_under_get_rule(kSecAttrKeyClass), key_class.to_value())); } if self.load_refs { params.push(( CFString::wrap_under_get_rule(kSecReturnRef), CFBoolean::true_value().into_CFType(), )); } if self.load_attributes { params.push(( CFString::wrap_under_get_rule(kSecReturnAttributes), CFBoolean::true_value().into_CFType(), )); } if self.load_data { params.push(( CFString::wrap_under_get_rule(kSecReturnData), CFBoolean::true_value().into_CFType(), )); } if let Some(limit) = self.limit { params.push(( CFString::wrap_under_get_rule(kSecMatchLimit), limit.to_value(), )); } if let Some(ref label) = self.label { params.push(( CFString::wrap_under_get_rule(kSecAttrLabel), label.as_CFType(), )); } if let Some(ref service) = self.service { params.push(( CFString::wrap_under_get_rule(kSecAttrService), service.as_CFType(), )); } if let Some(ref account) = self.account { params.push(( CFString::wrap_under_get_rule(kSecAttrAccount), account.as_CFType(), )); } if let Some(ref access_group) = self.access_group { params.push(( CFString::wrap_under_get_rule(kSecAttrAccessGroup), access_group.as_CFType(), )); } if let Some(ref pub_key_hash) = self.pub_key_hash { params.push(( CFString::wrap_under_get_rule(kSecAttrPublicKeyHash), pub_key_hash.as_CFType(), )); } if let Some(ref app_label) = self.app_label { params.push(( CFString::wrap_under_get_rule(kSecAttrApplicationLabel), app_label.as_CFType(), )); } let params = CFDictionary::from_CFType_pairs(¶ms); let mut ret = ptr::null(); cvt(SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret))?; if ret.is_null() { // SecItemCopyMatching returns NULL if no load_* was specified, // causing a segfault. return Ok(vec![]); } let type_id = CFGetTypeID(ret); let mut items = vec![]; if type_id == CFArray::::type_id() { let array: CFArray = CFArray::wrap_under_create_rule(ret as *mut _); for item in array.iter() { items.push(get_item(item.as_CFTypeRef())); } } else { items.push(get_item(ret)); // This is a bit janky, but get_item uses wrap_under_get_rule // which bumps the refcount but we want create semantics CFRelease(ret); } Ok(items) } } } unsafe fn get_item(item: CFTypeRef) -> SearchResult { let type_id = CFGetTypeID(item); if type_id == CFData::type_id() { let data = CFData::wrap_under_get_rule(item as *mut _); let mut buf = Vec::new(); buf.extend_from_slice(data.bytes()); return SearchResult::Data(buf); } if type_id == CFDictionary::<*const u8, *const u8>::type_id() { return SearchResult::Dict(CFDictionary::wrap_under_get_rule(item as *mut _)); } #[cfg(target_os = "macos")] { use crate::os::macos::keychain_item::SecKeychainItem; if type_id == SecKeychainItem::type_id() { return SearchResult::Ref(Reference::KeychainItem( SecKeychainItem::wrap_under_get_rule(item as *mut _), )); } } let reference = if type_id == SecCertificate::type_id() { Reference::Certificate(SecCertificate::wrap_under_get_rule(item as *mut _)) } else if type_id == SecKey::type_id() { Reference::Key(SecKey::wrap_under_get_rule(item as *mut _)) } else if type_id == SecIdentity::type_id() { Reference::Identity(SecIdentity::wrap_under_get_rule(item as *mut _)) } else { panic!("Got bad type from SecItemCopyMatching: {}", type_id); }; SearchResult::Ref(reference) } /// An enum including all objects whose references can be returned from a search. /// Note that generic _Keychain Items_, such as passwords and preferences, do /// not have specific object types; they are modeled using dictionaries and so /// are available directly as search results in variant `SearchResult::Dict`. #[derive(Debug)] pub enum Reference { /// A `SecIdentity`. Identity(SecIdentity), /// A `SecCertificate`. Certificate(SecCertificate), /// A `SecKey`. Key(SecKey), /// A `SecKeychainItem`. /// /// Only defined on OSX #[cfg(target_os = "macos")] KeychainItem(crate::os::macos::keychain_item::SecKeychainItem), #[doc(hidden)] __NonExhaustive, } /// An individual search result. pub enum SearchResult { /// A reference to the Security Framework object, if asked for. Ref(Reference), /// A dictionary of data about the Security Framework object, if asked for. Dict(CFDictionary), /// The Security Framework object as bytes, if asked for. Data(Vec), /// An unknown representation of the Security Framework object. Other, } impl fmt::Debug for SearchResult { #[cold] fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { Self::Ref(ref reference) => fmt .debug_struct("SearchResult::Ref") .field("reference", reference) .finish(), Self::Data(ref buf) => fmt .debug_struct("SearchResult::Data") .field("data", buf) .finish(), Self::Dict(_) => { let mut debug = fmt.debug_struct("SearchResult::Dict"); for (k, v) in self.simplify_dict().unwrap() { debug.field(&k, &v); } debug.finish() } Self::Other => write!(fmt, "SearchResult::Other"), } } } impl SearchResult { /// If the search result is a `CFDict`, simplify that to a /// `HashMap`. This transformation isn't /// comprehensive, it only supports `CFString`, `CFDate`, and `CFData` /// value types. #[must_use] pub fn simplify_dict(&self) -> Option> { match *self { Self::Dict(ref d) => unsafe { let mut retmap = HashMap::new(); let (keys, values) = d.get_keys_and_values(); for (k, v) in keys.iter().zip(values.iter()) { let keycfstr = CFString::wrap_under_get_rule((*k).cast()); let val: String = match CFGetTypeID(*v) { cfstring if cfstring == CFString::type_id() => { format!("{}", CFString::wrap_under_get_rule((*v).cast())) } cfdata if cfdata == CFData::type_id() => { let buf = CFData::wrap_under_get_rule((*v).cast()); let mut vec = Vec::new(); vec.extend_from_slice(buf.bytes()); format!("{}", String::from_utf8_lossy(&vec)) } cfdate if cfdate == CFDate::type_id() => format!( "{}", CFString::wrap_under_create_rule(CFCopyDescription(*v)) ), _ => String::from("unknown"), }; retmap.insert(format!("{}", keycfstr), val); } Some(retmap) }, _ => None, } } } /// Builder-pattern struct for specifying options for `add_item` (`SecAddItem` /// wrapper). /// /// When finished populating options, call `to_dictionary()` and pass the /// resulting `CFDictionary` to `add_item`. pub struct ItemAddOptions { /// The value (by ref or data) of the item to add, required. pub value: ItemAddValue, /// Optional kSecAttrLabel attribute. pub label: Option, /// Optional keychain location. pub location: Option, } impl ItemAddOptions { /// Specifies the item to add. #[must_use] pub fn new(value: ItemAddValue) -> Self { Self{ value, label: None, location: None } } /// Specifies the `kSecAttrLabel` attribute. pub fn set_label(&mut self, label: impl Into) -> &mut Self { self.label = Some(label.into()); self } /// Specifies which keychain to add the item to. pub fn set_location(&mut self, location: Location) -> &mut Self { self.location = Some(location); self } /// Populates a `CFDictionary` to be passed to pub fn to_dictionary(&self) -> CFDictionary { let mut dict = CFMutableDictionary::from_CFType_pairs(&[]); let class_opt = match &self.value { ItemAddValue::Ref(ref_) => ref_.class(), ItemAddValue::Data { class, .. } => Some(*class), }; if let Some(class) = class_opt { dict.add(&unsafe { kSecClass }.to_void(), &class.0.to_void()); } let value_pair = match &self.value { ItemAddValue::Ref(ref_) => (unsafe { kSecValueRef }.to_void(), ref_.ref_()), ItemAddValue::Data { data, .. } => (unsafe { kSecValueData }.to_void(), data.to_void()), }; dict.add(&value_pair.0, &value_pair.1); if let Some(location) = &self.location { match location { #[cfg(any(feature = "OSX_10_15", target_os = "ios"))] Location::DataProtectionKeychain => { dict.add( &unsafe { kSecUseDataProtectionKeychain }.to_void(), &CFBoolean::true_value().to_void(), ); } #[cfg(target_os = "macos")] Location::DefaultFileKeychain => {} #[cfg(target_os = "macos")] Location::FileKeychain(keychain) => { dict.add(&unsafe { kSecUseKeychain }.to_void(), &keychain.to_void()); }, } } let label = self.label.as_deref().map(CFString::from); if let Some(label) = &label { dict.add(&unsafe { kSecAttrLabel }.to_void(), &label.to_void()); } dict.to_immutable() } } /// Value of an item to add to the keychain. pub enum ItemAddValue { /// Pass item by Ref (kSecValueRef) Ref(AddRef), /// Pass item by Data (kSecValueData) Data { /// The item class (kSecClass). class: ItemClass, /// The item data. data: CFData, }, } /// Type of Ref to add to the keychain. pub enum AddRef { /// SecKey Key(SecKey), /// SecIdentity Identity(SecIdentity), /// SecCertificate Certificate(SecCertificate), } impl AddRef { fn class(&self) -> Option { match self { AddRef::Key(_) => Some(ItemClass::key()), // kSecClass should not be specified when adding a SecIdentityRef: // https://developer.apple.com/forums/thread/25751 AddRef::Identity(_) => None, AddRef::Certificate(_) => Some(ItemClass::certificate()), } } fn ref_(&self) -> CFTypeRef { match self { AddRef::Key(key) => key.as_CFTypeRef(), AddRef::Identity(id) => id.as_CFTypeRef(), AddRef::Certificate(cert) => cert.as_CFTypeRef(), } } } /// Which keychain to add an item to. /// /// pub enum Location { /// Store the item in the newer DataProtectionKeychain. This is the only /// keychain on iOS. On macOS, this is the newer and more consistent /// keychain implementation. Keys stored in the Secure Enclave _must_ use /// this keychain. /// /// This keychain requires the calling binary to be codesigned with /// entitlements for the KeychainAccessGroups it is supposed to /// access. #[cfg(any(feature = "OSX_10_15", target_os = "ios"))] DataProtectionKeychain, /// Store the key in the default file-based keychain. On macOS, defaults to /// the Login keychain. #[cfg(target_os = "macos")] DefaultFileKeychain, /// Store the key in a specific file-based keychain. #[cfg(target_os = "macos")] FileKeychain(crate::os::macos::keychain::SecKeychain), } /// Translates to `SecItemAdd`. Use `ItemAddOptions` to build an `add_params` /// `CFDictionary`. pub fn add_item(add_params: CFDictionary) -> Result<()> { cvt(unsafe { SecItemAdd(add_params.as_concrete_TypeRef(), std::ptr::null_mut()) }) } #[cfg(test)] mod test { use super::*; #[test] fn find_nothing() { assert!(ItemSearchOptions::new().search().is_err()); } #[test] fn limit_two() { let results = ItemSearchOptions::new() .class(ItemClass::certificate()) .limit(2) .search() .unwrap(); assert_eq!(results.len(), 2); } #[test] fn limit_all() { let results = ItemSearchOptions::new() .class(ItemClass::certificate()) .limit(Limit::All) .search() .unwrap(); assert!(results.len() >= 2); } }