diff options
Diffstat (limited to 'vendor/security-framework/src/item.rs')
-rw-r--r-- | vendor/security-framework/src/item.rs | 689 |
1 files changed, 689 insertions, 0 deletions
diff --git a/vendor/security-framework/src/item.rs b/vendor/security-framework/src/item.rs new file mode 100644 index 000000000..6f252e00f --- /dev/null +++ b/vendor/security-framework/src/item.rs @@ -0,0 +1,689 @@ +//! 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); + } +} |