| //! 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<i64> 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<CFArray<SecKeychain>>, |
| #[cfg(not(target_os = "macos"))] |
| keychains: Option<CFArray<CFType>>, |
| class: Option<ItemClass>, |
| key_class: Option<KeyClass>, |
| load_refs: bool, |
| load_attributes: bool, |
| load_data: bool, |
| limit: Option<Limit>, |
| label: Option<CFString>, |
| service: Option<CFString>, |
| account: Option<CFString>, |
| access_group: Option<CFString>, |
| pub_key_hash: Option<CFData>, |
| app_label: Option<CFData>, |
| } |
| |
| #[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<T: Into<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<Vec<SearchResult>> { |
| 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::<CFType>::type_id() { |
| let array: CFArray<CFType> = 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<u8>), |
| /// 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<String, String>`. This transformation isn't |
| /// comprehensive, it only supports `CFString`, `CFDate`, and `CFData` |
| /// value types. |
| #[must_use] |
| pub fn simplify_dict(&self) -> Option<HashMap<String, String>> { |
| 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<String>, |
| /// Optional keychain location. |
| pub location: Option<Location>, |
| } |
| |
| 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<String>) -> &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<ItemClass> { |
| 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. |
| /// |
| /// <https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains> |
| 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); |
| } |
| } |