apple_security/
item.rs

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