apple_security_framework/
item.rs

1//! Support to search for items in a keychain.
2
3use std::{collections::HashMap, fmt, ptr};
4
5use core_foundation::{
6    array::CFArray,
7    base::{CFType, TCFType, ToVoid},
8    boolean::CFBoolean,
9    data::CFData,
10    date::CFDate,
11    dictionary::{CFDictionary, CFMutableDictionary},
12    number::CFNumber,
13    string::CFString,
14};
15use core_foundation_sys::{
16    base::{CFCopyDescription, CFGetTypeID, CFRelease, CFTypeRef},
17    string::CFStringRef,
18};
19use security_framework_sys::{
20    item::*,
21    keychain_item::{SecItemAdd, SecItemCopyMatching},
22};
23
24#[cfg(target_os = "macos")]
25use crate::os::macos::keychain::SecKeychain;
26use crate::{base::Result, certificate::SecCertificate, cvt, identity::SecIdentity, key::SecKey};
27
28/// Specifies the type of items to search for.
29#[derive(Debug, Copy, Clone)]
30pub struct ItemClass(CFStringRef);
31
32impl ItemClass {
33    /// Look for `SecKeychainItem`s corresponding to generic passwords.
34    #[inline(always)]
35    #[must_use]
36    pub fn generic_password() -> Self {
37        unsafe { Self(kSecClassGenericPassword) }
38    }
39
40    /// Look for `SecKeychainItem`s corresponding to internet passwords.
41    #[inline(always)]
42    #[must_use]
43    pub fn internet_password() -> Self {
44        unsafe { Self(kSecClassInternetPassword) }
45    }
46
47    /// Look for `SecCertificate`s.
48    #[inline(always)]
49    #[must_use]
50    pub fn certificate() -> Self {
51        unsafe { Self(kSecClassCertificate) }
52    }
53
54    /// Look for `SecKey`s.
55    #[inline(always)]
56    #[must_use]
57    pub fn key() -> Self {
58        unsafe { Self(kSecClassKey) }
59    }
60
61    /// Look for `SecIdentity`s.
62    #[inline(always)]
63    #[must_use]
64    pub fn identity() -> Self {
65        unsafe { Self(kSecClassIdentity) }
66    }
67
68    #[inline]
69    fn to_value(self) -> CFType {
70        unsafe { CFType::wrap_under_get_rule(self.0.cast()) }
71    }
72}
73
74/// Specifies the type of keys to search for.
75#[derive(Debug, Copy, Clone)]
76pub struct KeyClass(CFStringRef);
77
78impl KeyClass {
79    /// `kSecAttrKeyClassPublic`
80    #[inline(always)]
81    #[must_use]
82    pub fn public() -> Self {
83        unsafe { Self(kSecAttrKeyClassPublic) }
84    }
85    /// `kSecAttrKeyClassPrivate`
86    #[inline(always)]
87    #[must_use]
88    pub fn private() -> Self {
89        unsafe { Self(kSecAttrKeyClassPrivate) }
90    }
91    /// `kSecAttrKeyClassSymmetric`
92    #[inline(always)]
93    #[must_use]
94    pub fn symmetric() -> Self {
95        unsafe { Self(kSecAttrKeyClassSymmetric) }
96    }
97
98    #[inline]
99    fn to_value(self) -> CFType {
100        unsafe { CFType::wrap_under_get_rule(self.0.cast()) }
101    }
102}
103
104/// Specifies the number of results returned by a search
105#[derive(Debug, Copy, Clone)]
106pub enum Limit {
107    /// Always return all results
108    All,
109
110    /// Return up to the specified number of results
111    Max(i64),
112}
113
114impl Limit {
115    #[inline]
116    fn to_value(self) -> CFType {
117        match self {
118            Self::All => unsafe { CFString::wrap_under_get_rule(kSecMatchLimitAll).into_CFType() },
119            Self::Max(l) => CFNumber::from(l).into_CFType(),
120        }
121    }
122}
123
124impl From<i64> for Limit {
125    #[inline]
126    fn from(limit: i64) -> Self {
127        Self::Max(limit)
128    }
129}
130
131/// A builder type to search for items in keychains.
132#[derive(Default)]
133pub struct ItemSearchOptions {
134    #[cfg(target_os = "macos")]
135    keychains: Option<CFArray<SecKeychain>>,
136    #[cfg(not(target_os = "macos"))]
137    keychains: Option<CFArray<CFType>>,
138    class: Option<ItemClass>,
139    key_class: Option<KeyClass>,
140    load_refs: bool,
141    load_attributes: bool,
142    load_data: bool,
143    limit: Option<Limit>,
144    label: Option<CFString>,
145    service: Option<CFString>,
146    account: Option<CFString>,
147    access_group: Option<CFString>,
148    pub_key_hash: Option<CFData>,
149    app_label: Option<CFData>,
150}
151
152#[cfg(target_os = "macos")]
153impl crate::ItemSearchOptionsInternals for ItemSearchOptions {
154    #[inline]
155    fn keychains(&mut self, keychains: &[SecKeychain]) -> &mut Self {
156        self.keychains = Some(CFArray::from_CFTypes(keychains));
157        self
158    }
159}
160
161impl ItemSearchOptions {
162    /// Creates a new builder with default options.
163    #[inline(always)]
164    #[must_use]
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// Search only for items of the specified class.
170    #[inline(always)]
171    pub fn class(&mut self, class: ItemClass) -> &mut Self {
172        self.class = Some(class);
173        self
174    }
175
176    /// Search only for keys of the specified class. Also sets self.class to
177    /// ItemClass::key().
178    #[inline(always)]
179    pub fn key_class(&mut self, key_class: KeyClass) -> &mut Self {
180        self.class(ItemClass::key());
181        self.key_class = Some(key_class);
182        self
183    }
184
185    /// Load Security Framework objects (`SecCertificate`, `SecKey`, etc) for
186    /// the results.
187    #[inline(always)]
188    pub fn load_refs(&mut self, load_refs: bool) -> &mut Self {
189        self.load_refs = load_refs;
190        self
191    }
192
193    /// Load Security Framework object attributes for
194    /// the results.
195    #[inline(always)]
196    pub fn load_attributes(&mut self, load_attributes: bool) -> &mut Self {
197        self.load_attributes = load_attributes;
198        self
199    }
200
201    /// Load Security Framework objects data for
202    /// the results.
203    #[inline(always)]
204    pub fn load_data(&mut self, load_data: bool) -> &mut Self {
205        self.load_data = load_data;
206        self
207    }
208
209    /// Limit the number of search results.
210    ///
211    /// If this is not called, the default limit is 1.
212    #[inline(always)]
213    pub fn limit<T: Into<Limit>>(&mut self, limit: T) -> &mut Self {
214        self.limit = Some(limit.into());
215        self
216    }
217
218    /// Search for an item with the given label.
219    #[inline(always)]
220    pub fn label(&mut self, label: &str) -> &mut Self {
221        self.label = Some(CFString::new(label));
222        self
223    }
224
225    /// Search for an item with the given service.
226    #[inline(always)]
227    pub fn service(&mut self, service: &str) -> &mut Self {
228        self.service = Some(CFString::new(service));
229        self
230    }
231
232    /// Search for an item with the given account.
233    #[inline(always)]
234    pub fn account(&mut self, account: &str) -> &mut Self {
235        self.account = Some(CFString::new(account));
236        self
237    }
238
239    /// Sets `kSecAttrAccessGroup` to `kSecAttrAccessGroupToken`
240    #[inline(always)]
241    pub fn access_group_token(&mut self) -> &mut Self {
242        self.access_group =
243            unsafe { Some(CFString::wrap_under_get_rule(kSecAttrAccessGroupToken)) };
244        self
245    }
246
247    /// Search for a certificate with the given public key hash.
248    ///
249    /// This is only compatible with [`ItemClass::certificate`], to search for
250    /// a key by public key hash use [`ItemSearchOptions::application_label`]
251    /// instead.
252    #[inline(always)]
253    pub fn pub_key_hash(&mut self, pub_key_hash: &[u8]) -> &mut Self {
254        self.pub_key_hash = Some(CFData::from_buffer(pub_key_hash));
255        self
256    }
257
258    /// Search for a key with the given public key hash.
259    ///
260    /// This is only compatible with [`ItemClass::key`], to search for a
261    /// certificate by the public key hash use [`ItemSearchOptions::pub_key_hash`]
262    /// instead.
263    #[inline(always)]
264    pub fn application_label(&mut self, app_label: &[u8]) -> &mut Self {
265        self.app_label = Some(CFData::from_buffer(app_label));
266        self
267    }
268
269    /// Search for objects.
270    pub fn search(&self) -> Result<Vec<SearchResult>> {
271        unsafe {
272            let mut params = vec![];
273
274            if let Some(ref keychains) = self.keychains {
275                params.push((
276                    CFString::wrap_under_get_rule(kSecMatchSearchList),
277                    keychains.as_CFType(),
278                ));
279            }
280
281            if let Some(class) = self.class {
282                params.push((CFString::wrap_under_get_rule(kSecClass), class.to_value()));
283            }
284
285            if let Some(key_class) = self.key_class {
286                params.push((
287                    CFString::wrap_under_get_rule(kSecAttrKeyClass),
288                    key_class.to_value(),
289                ));
290            }
291
292            if self.load_refs {
293                params.push((
294                    CFString::wrap_under_get_rule(kSecReturnRef),
295                    CFBoolean::true_value().into_CFType(),
296                ));
297            }
298
299            if self.load_attributes {
300                params.push((
301                    CFString::wrap_under_get_rule(kSecReturnAttributes),
302                    CFBoolean::true_value().into_CFType(),
303                ));
304            }
305
306            if self.load_data {
307                params.push((
308                    CFString::wrap_under_get_rule(kSecReturnData),
309                    CFBoolean::true_value().into_CFType(),
310                ));
311            }
312
313            if let Some(limit) = self.limit {
314                params.push((
315                    CFString::wrap_under_get_rule(kSecMatchLimit),
316                    limit.to_value(),
317                ));
318            }
319
320            if let Some(ref label) = self.label {
321                params.push((
322                    CFString::wrap_under_get_rule(kSecAttrLabel),
323                    label.as_CFType(),
324                ));
325            }
326
327            if let Some(ref service) = self.service {
328                params.push((
329                    CFString::wrap_under_get_rule(kSecAttrService),
330                    service.as_CFType(),
331                ));
332            }
333
334            if let Some(ref account) = self.account {
335                params.push((
336                    CFString::wrap_under_get_rule(kSecAttrAccount),
337                    account.as_CFType(),
338                ));
339            }
340
341            if let Some(ref access_group) = self.access_group {
342                params.push((
343                    CFString::wrap_under_get_rule(kSecAttrAccessGroup),
344                    access_group.as_CFType(),
345                ));
346            }
347
348            if let Some(ref pub_key_hash) = self.pub_key_hash {
349                params.push((
350                    CFString::wrap_under_get_rule(kSecAttrPublicKeyHash),
351                    pub_key_hash.as_CFType(),
352                ));
353            }
354
355            if let Some(ref app_label) = self.app_label {
356                params.push((
357                    CFString::wrap_under_get_rule(kSecAttrApplicationLabel),
358                    app_label.as_CFType(),
359                ));
360            }
361
362            let params = CFDictionary::from_CFType_pairs(&params);
363
364            let mut ret = ptr::null();
365            cvt(SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret))?;
366            if ret.is_null() {
367                //  SecItemCopyMatching returns NULL if no load_* was specified,
368                //  causing a segfault.
369                return Ok(vec![]);
370            }
371            let type_id = CFGetTypeID(ret);
372
373            let mut items = vec![];
374
375            if type_id == CFArray::<CFType>::type_id() {
376                let array: CFArray<CFType> = CFArray::wrap_under_create_rule(ret as *mut _);
377                for item in array.iter() {
378                    items.push(get_item(item.as_CFTypeRef()));
379                }
380            } else {
381                items.push(get_item(ret));
382                // This is a bit janky, but get_item uses wrap_under_get_rule
383                // which bumps the refcount but we want create semantics
384                CFRelease(ret);
385            }
386
387            Ok(items)
388        }
389    }
390}
391
392unsafe fn get_item(item: CFTypeRef) -> SearchResult {
393    let type_id = CFGetTypeID(item);
394
395    if type_id == CFData::type_id() {
396        let data = CFData::wrap_under_get_rule(item as *mut _);
397        let mut buf = Vec::new();
398        buf.extend_from_slice(data.bytes());
399        return SearchResult::Data(buf);
400    }
401
402    if type_id == CFDictionary::<*const u8, *const u8>::type_id() {
403        return SearchResult::Dict(CFDictionary::wrap_under_get_rule(item as *mut _));
404    }
405
406    #[cfg(target_os = "macos")]
407    {
408        use crate::os::macos::keychain_item::SecKeychainItem;
409        if type_id == SecKeychainItem::type_id() {
410            return SearchResult::Ref(Reference::KeychainItem(
411                SecKeychainItem::wrap_under_get_rule(item as *mut _),
412            ));
413        }
414    }
415
416    let reference = if type_id == SecCertificate::type_id() {
417        Reference::Certificate(SecCertificate::wrap_under_get_rule(item as *mut _))
418    } else if type_id == SecKey::type_id() {
419        Reference::Key(SecKey::wrap_under_get_rule(item as *mut _))
420    } else if type_id == SecIdentity::type_id() {
421        Reference::Identity(SecIdentity::wrap_under_get_rule(item as *mut _))
422    } else {
423        panic!("Got bad type from SecItemCopyMatching: {}", type_id);
424    };
425
426    SearchResult::Ref(reference)
427}
428
429/// An enum including all objects whose references can be returned from a search.
430/// Note that generic _Keychain Items_, such as passwords and preferences, do
431/// not have specific object types; they are modeled using dictionaries and so
432/// are available directly as search results in variant `SearchResult::Dict`.
433#[derive(Debug)]
434#[non_exhaustive]
435pub enum Reference {
436    /// A `SecIdentity`.
437    Identity(SecIdentity),
438
439    /// A `SecCertificate`.
440    Certificate(SecCertificate),
441
442    /// A `SecKey`.
443    Key(SecKey),
444
445    /// A `SecKeychainItem`.
446    ///
447    /// Only defined on OSX
448    #[cfg(target_os = "macos")]
449    KeychainItem(crate::os::macos::keychain_item::SecKeychainItem),
450}
451
452/// An individual search result.
453pub enum SearchResult {
454    /// A reference to the Security Framework object, if asked for.
455    Ref(Reference),
456    /// A dictionary of data about the Security Framework object, if asked for.
457    Dict(CFDictionary),
458    /// The Security Framework object as bytes, if asked for.
459    Data(Vec<u8>),
460    /// An unknown representation of the Security Framework object.
461    Other,
462}
463
464impl fmt::Debug for SearchResult {
465    #[cold]
466    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
467        match *self {
468            Self::Ref(ref reference) => fmt
469                .debug_struct("SearchResult::Ref")
470                .field("reference", reference)
471                .finish(),
472            Self::Data(ref buf) => fmt
473                .debug_struct("SearchResult::Data")
474                .field("data", buf)
475                .finish(),
476            Self::Dict(_) => {
477                let mut debug = fmt.debug_struct("SearchResult::Dict");
478                for (k, v) in self.simplify_dict().unwrap() {
479                    debug.field(&k, &v);
480                }
481                debug.finish()
482            }
483            Self::Other => write!(fmt, "SearchResult::Other"),
484        }
485    }
486}
487
488impl SearchResult {
489    /// If the search result is a `CFDict`, simplify that to a
490    /// `HashMap<String, String>`. This transformation isn't
491    /// comprehensive, it only supports `CFString`, `CFDate`, and `CFData`
492    /// value types.
493    #[must_use]
494    pub fn simplify_dict(&self) -> Option<HashMap<String, String>> {
495        match *self {
496            Self::Dict(ref d) => unsafe {
497                let mut retmap = HashMap::new();
498                let (keys, values) = d.get_keys_and_values();
499                for (k, v) in keys.iter().zip(values.iter()) {
500                    let keycfstr = CFString::wrap_under_get_rule((*k).cast());
501                    let val: String = match CFGetTypeID(*v) {
502                        cfstring if cfstring == CFString::type_id() => {
503                            format!("{}", CFString::wrap_under_get_rule((*v).cast()))
504                        }
505                        cfdata if cfdata == CFData::type_id() => {
506                            let buf = CFData::wrap_under_get_rule((*v).cast());
507                            let mut vec = Vec::new();
508                            vec.extend_from_slice(buf.bytes());
509                            format!("{}", String::from_utf8_lossy(&vec))
510                        }
511                        cfdate if cfdate == CFDate::type_id() => format!(
512                            "{}",
513                            CFString::wrap_under_create_rule(CFCopyDescription(*v))
514                        ),
515                        _ => String::from("unknown"),
516                    };
517                    retmap.insert(format!("{}", keycfstr), val);
518                }
519                Some(retmap)
520            },
521            _ => None,
522        }
523    }
524}
525
526/// Builder-pattern struct for specifying options for `add_item` (`SecAddItem`
527/// wrapper).
528///
529/// When finished populating options, call `to_dictionary()` and pass the
530/// resulting `CFDictionary` to `add_item`.
531pub struct ItemAddOptions {
532    /// The value (by ref or data) of the item to add, required.
533    pub value: ItemAddValue,
534    /// Optional kSecAttrLabel attribute.
535    pub label: Option<String>,
536    /// Optional keychain location.
537    pub location: Option<Location>,
538}
539
540impl ItemAddOptions {
541    /// Specifies the item to add.
542    #[must_use]
543    pub fn new(value: ItemAddValue) -> Self {
544        Self {
545            value,
546            label: None,
547            location: None,
548        }
549    }
550    /// Specifies the `kSecAttrLabel` attribute.
551    pub fn set_label(&mut self, label: impl Into<String>) -> &mut Self {
552        self.label = Some(label.into());
553        self
554    }
555    /// Specifies which keychain to add the item to.
556    pub fn set_location(&mut self, location: Location) -> &mut Self {
557        self.location = Some(location);
558        self
559    }
560    /// Populates a `CFDictionary` to be passed to
561    pub fn to_dictionary(&self) -> CFDictionary {
562        let mut dict = CFMutableDictionary::from_CFType_pairs(&[]);
563
564        let class_opt = match &self.value {
565            ItemAddValue::Ref(ref_) => ref_.class(),
566            ItemAddValue::Data { class, .. } => Some(*class),
567        };
568        if let Some(class) = class_opt {
569            dict.add(&unsafe { kSecClass }.to_void(), &class.0.to_void());
570        }
571
572        let value_pair = match &self.value {
573            ItemAddValue::Ref(ref_) => (unsafe { kSecValueRef }.to_void(), ref_.ref_()),
574            ItemAddValue::Data { data, .. } => (unsafe { kSecValueData }.to_void(), data.to_void()),
575        };
576        dict.add(&value_pair.0, &value_pair.1);
577
578        if let Some(location) = &self.location {
579            match location {
580                #[cfg(any(feature = "OSX_10_15", target_os = "ios"))]
581                Location::DataProtectionKeychain => {
582                    dict.add(
583                        &unsafe { kSecUseDataProtectionKeychain }.to_void(),
584                        &CFBoolean::true_value().to_void(),
585                    );
586                }
587                #[cfg(target_os = "macos")]
588                Location::DefaultFileKeychain => {}
589                #[cfg(target_os = "macos")]
590                Location::FileKeychain(keychain) => {
591                    dict.add(&unsafe { kSecUseKeychain }.to_void(), &keychain.to_void());
592                }
593            }
594        }
595
596        let label = self.label.as_deref().map(CFString::from);
597        if let Some(label) = &label {
598            dict.add(&unsafe { kSecAttrLabel }.to_void(), &label.to_void());
599        }
600
601        dict.to_immutable()
602    }
603}
604
605/// Value of an item to add to the keychain.
606pub enum ItemAddValue {
607    /// Pass item by Ref (kSecValueRef)
608    Ref(AddRef),
609    /// Pass item by Data (kSecValueData)
610    Data {
611        /// The item class (kSecClass).
612        class: ItemClass,
613        /// The item data.
614        data: CFData,
615    },
616}
617
618/// Type of Ref to add to the keychain.
619pub enum AddRef {
620    /// SecKey
621    Key(SecKey),
622    /// SecIdentity
623    Identity(SecIdentity),
624    /// SecCertificate
625    Certificate(SecCertificate),
626}
627
628impl AddRef {
629    fn class(&self) -> Option<ItemClass> {
630        match self {
631            AddRef::Key(_) => Some(ItemClass::key()),
632            //  kSecClass should not be specified when adding a SecIdentityRef:
633            //  https://developer.apple.com/forums/thread/25751
634            AddRef::Identity(_) => None,
635            AddRef::Certificate(_) => Some(ItemClass::certificate()),
636        }
637    }
638    fn ref_(&self) -> CFTypeRef {
639        match self {
640            AddRef::Key(key) => key.as_CFTypeRef(),
641            AddRef::Identity(id) => id.as_CFTypeRef(),
642            AddRef::Certificate(cert) => cert.as_CFTypeRef(),
643        }
644    }
645}
646
647/// Which keychain to add an item to.
648///
649/// <https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains>
650pub enum Location {
651    /// Store the item in the newer DataProtectionKeychain. This is the only
652    /// keychain on iOS. On macOS, this is the newer and more consistent
653    /// keychain implementation. Keys stored in the Secure Enclave _must_ use
654    /// this keychain.
655    ///
656    /// This keychain requires the calling binary to be codesigned with
657    /// entitlements for the KeychainAccessGroups it is supposed to
658    /// access.
659    #[cfg(any(feature = "OSX_10_15", target_os = "ios"))]
660    DataProtectionKeychain,
661    /// Store the key in the default file-based keychain. On macOS, defaults to
662    /// the Login keychain.
663    #[cfg(target_os = "macos")]
664    DefaultFileKeychain,
665    /// Store the key in a specific file-based keychain.
666    #[cfg(target_os = "macos")]
667    FileKeychain(crate::os::macos::keychain::SecKeychain),
668}
669
670/// Translates to `SecItemAdd`. Use `ItemAddOptions` to build an `add_params`
671/// `CFDictionary`.
672pub fn add_item(add_params: CFDictionary) -> Result<()> {
673    cvt(unsafe { SecItemAdd(add_params.as_concrete_TypeRef(), std::ptr::null_mut()) })
674}
675
676#[cfg(test)]
677mod test {
678    use super::*;
679
680    #[test]
681    fn find_nothing() {
682        assert!(ItemSearchOptions::new().search().is_err());
683    }
684
685    #[test]
686    fn limit_two() {
687        let results = ItemSearchOptions::new()
688            .class(ItemClass::certificate())
689            .limit(2)
690            .search()
691            .unwrap();
692        assert_eq!(results.len(), 2);
693    }
694
695    #[test]
696    fn limit_all() {
697        let results = ItemSearchOptions::new()
698            .class(ItemClass::certificate())
699            .limit(Limit::All)
700            .search()
701            .unwrap();
702        assert!(results.len() >= 2);
703    }
704}