Skip to main content

security_framework/
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 security_framework_sys::item::*;
14use security_framework_sys::keychain_item::{
15    SecItemAdd, SecItemCopyMatching, SecItemDelete, SecItemUpdate,
16};
17use std::collections::HashMap;
18use std::fmt;
19use std::ptr;
20
21use crate::base::Result;
22use crate::certificate::SecCertificate;
23use crate::cvt;
24use crate::identity::SecIdentity;
25use crate::key::SecKey;
26#[cfg(target_os = "macos")]
27use crate::os::macos::keychain::SecKeychain;
28
29/// Specifies the type of items to search for.
30#[derive(Debug, Copy, Clone)]
31pub struct ItemClass(CFStringRef);
32
33impl ItemClass {
34    /// Look for `SecKeychainItem`s corresponding to generic passwords.
35    #[inline(always)]
36    #[must_use]
37    pub fn generic_password() -> Self {
38        unsafe { Self(kSecClassGenericPassword) }
39    }
40
41    /// Look for `SecKeychainItem`s corresponding to internet passwords.
42    #[inline(always)]
43    #[must_use]
44    pub fn internet_password() -> Self {
45        unsafe { Self(kSecClassInternetPassword) }
46    }
47
48    /// Look for `SecCertificate`s.
49    #[inline(always)]
50    #[must_use]
51    pub fn certificate() -> Self {
52        unsafe { Self(kSecClassCertificate) }
53    }
54
55    /// Look for `SecKey`s.
56    #[inline(always)]
57    #[must_use]
58    pub fn key() -> Self {
59        unsafe { Self(kSecClassKey) }
60    }
61
62    /// Look for `SecIdentity`s.
63    #[inline(always)]
64    #[must_use]
65    pub fn identity() -> Self {
66        unsafe { Self(kSecClassIdentity) }
67    }
68}
69
70/// Specifies the type of keys to search for.
71#[derive(Debug, Copy, Clone)]
72pub struct KeyClass(CFStringRef);
73
74impl KeyClass {
75    /// `kSecAttrKeyClassPublic`
76    #[inline(always)]
77    #[must_use]
78    pub fn public() -> Self {
79        unsafe { Self(kSecAttrKeyClassPublic) }
80    }
81
82    /// `kSecAttrKeyClassPrivate`
83    #[inline(always)]
84    #[must_use]
85    pub fn private() -> Self {
86        unsafe { Self(kSecAttrKeyClassPrivate) }
87    }
88
89    /// `kSecAttrKeyClassSymmetric`
90    #[inline(always)]
91    #[must_use]
92    pub fn symmetric() -> Self {
93        unsafe { Self(kSecAttrKeyClassSymmetric) }
94    }
95}
96
97/// Specifies the number of results returned by a search
98#[derive(Debug, Copy, Clone)]
99pub enum Limit {
100    /// Always return all results
101    All,
102
103    /// Return up to the specified number of results
104    Max(i64),
105}
106
107impl Limit {
108    #[inline]
109    fn to_value(self) -> CFType {
110        match self {
111            Self::All => unsafe { CFString::wrap_under_get_rule(kSecMatchLimitAll).into_CFType() },
112            Self::Max(l) => CFNumber::from(l).into_CFType(),
113        }
114    }
115}
116
117impl From<i64> for Limit {
118    #[inline]
119    fn from(limit: i64) -> Self {
120        Self::Max(limit)
121    }
122}
123
124/// Specifies whether a search should match cloud-synchronized items.
125#[derive(Debug, Copy, Clone)]
126pub enum CloudSync {
127    /// Match only items that are cloud-synchronized.
128    MatchSyncYes,
129    /// Match only items that are not cloud-synchronized.
130    MatchSyncNo,
131    /// Match items whether they are cloud-synchronized or not.
132    MatchSyncAny,
133}
134
135impl From<Option<bool>> for CloudSync {
136    #[inline]
137    fn from(is_sync: Option<bool>) -> Self {
138        match is_sync {
139            Some(true) => Self::MatchSyncYes,
140            Some(false) => Self::MatchSyncNo,
141            None => Self::MatchSyncAny,
142        }
143    }
144}
145
146/// A builder type to search for items in keychains.
147#[derive(Default)]
148pub struct ItemSearchOptions {
149    #[cfg(target_os = "macos")]
150    keychains: Option<CFArray<SecKeychain>>,
151    #[cfg(not(target_os = "macos"))]
152    keychains: Option<CFArray<CFType>>,
153    ignore_legacy_keychains: bool, // defined everywhere, only consulted on macOS
154    case_insensitive: Option<bool>,
155    class: Option<ItemClass>,
156    key_class: Option<KeyClass>,
157    load_refs: bool,
158    load_attributes: bool,
159    load_data: bool,
160    limit: Option<Limit>,
161    trusted_only: Option<bool>,
162    label: Option<CFString>,
163    service: Option<CFString>,
164    subject: Option<CFString>,
165    account: Option<CFString>,
166    access_group: Option<CFString>,
167    cloud_sync: Option<CloudSync>,
168    pub_key_hash: Option<CFData>,
169    serial_number: Option<CFData>,
170    app_label: Option<CFData>,
171    authentication_context: Option<CFType>,
172    skip_authenticated_items: bool,
173}
174
175#[cfg(target_os = "macos")]
176impl ItemSearchOptions {
177    /// Search within the specified macOS keychains.
178    ///
179    /// If this is not called, the default keychain will be searched.
180    #[inline]
181    pub fn keychains(&mut self, keychains: &[SecKeychain]) -> &mut Self {
182        self.keychains = Some(CFArray::from_CFTypes(keychains));
183        self
184    }
185
186    /// Only search the protected data macOS keychains.
187    ///
188    /// Has no effect if a legacy keychain has been explicitly specified
189    /// using [keychains](ItemSearchOptions::keychains).
190    ///
191    /// Has no effect except in sandboxed applications on macOS 10.15 and above
192    #[inline]
193    pub fn ignore_legacy_keychains(&mut self) -> &mut Self {
194        self.ignore_legacy_keychains = true;
195        self
196    }
197}
198
199impl ItemSearchOptions {
200    /// Creates a new builder with default options.
201    #[inline(always)]
202    #[must_use]
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    /// Search only for items of the specified class.
208    #[inline(always)]
209    pub fn class(&mut self, class: ItemClass) -> &mut Self {
210        self.class = Some(class);
211        self
212    }
213
214    /// Whether search for an item should be case insensitive or not.
215    #[inline(always)]
216    pub fn case_insensitive(&mut self, case_insensitive: Option<bool>) -> &mut Self {
217        self.case_insensitive = case_insensitive;
218        self
219    }
220
221    /// Search only for keys of the specified class. Also sets self.class to
222    /// `ItemClass::key()`.
223    #[inline(always)]
224    pub fn key_class(&mut self, key_class: KeyClass) -> &mut Self {
225        self.class(ItemClass::key());
226        self.key_class = Some(key_class);
227        self
228    }
229
230    /// Load Security Framework objects (`SecCertificate`, `SecKey`, etc) for
231    /// the results.
232    #[inline(always)]
233    pub fn load_refs(&mut self, load_refs: bool) -> &mut Self {
234        self.load_refs = load_refs;
235        self
236    }
237
238    /// Load Security Framework object attributes for
239    /// the results.
240    #[inline(always)]
241    pub fn load_attributes(&mut self, load_attributes: bool) -> &mut Self {
242        self.load_attributes = load_attributes;
243        self
244    }
245
246    /// Load Security Framework objects data for
247    /// the results.
248    #[inline(always)]
249    pub fn load_data(&mut self, load_data: bool) -> &mut Self {
250        self.load_data = load_data;
251        self
252    }
253
254    /// Limit the number of search results.
255    ///
256    /// If this is not called, the default limit is 1.
257    #[inline(always)]
258    pub fn limit<T: Into<Limit>>(&mut self, limit: T) -> &mut Self {
259        self.limit = Some(limit.into());
260        self
261    }
262
263    /// Search for an item with the given label.
264    #[inline(always)]
265    pub fn label(&mut self, label: &str) -> &mut Self {
266        self.label = Some(CFString::new(label));
267        self
268    }
269
270    /// Whether untrusted certificates should be returned.
271    #[inline(always)]
272    pub fn trusted_only(&mut self, trusted_only: Option<bool>) -> &mut Self {
273        self.trusted_only = trusted_only;
274        self
275    }
276
277    /// Search for an item with the given service.
278    #[inline(always)]
279    pub fn service(&mut self, service: &str) -> &mut Self {
280        self.service = Some(CFString::new(service));
281        self
282    }
283
284    /// Search for an item with exactly the given subject.
285    #[inline(always)]
286    pub fn subject(&mut self, subject: &str) -> &mut Self {
287        self.subject = Some(CFString::new(subject));
288        self
289    }
290
291    /// Search for an item with the given account.
292    #[inline(always)]
293    pub fn account(&mut self, account: &str) -> &mut Self {
294        self.account = Some(CFString::new(account));
295        self
296    }
297
298    /// Search for an item with a specific access group.
299    pub fn access_group(&mut self, access_group: &str) -> &mut Self {
300        self.access_group = Some(CFString::new(access_group));
301        self
302    }
303
304    /// Search for an item based on whether it's cloud-synchronized
305    ///
306    /// If not specified, only searches non-synchronized entries.
307    pub fn cloud_sync<T: Into<CloudSync>>(&mut self, spec: T) -> &mut Self {
308        self.cloud_sync = Some(spec.into());
309        self
310    }
311
312    /// Sets `kSecAttrAccessGroup` to `kSecAttrAccessGroupToken`
313    #[inline(always)]
314    pub fn access_group_token(&mut self) -> &mut Self {
315        self.access_group = unsafe { Some(CFString::wrap_under_get_rule(kSecAttrAccessGroupToken)) };
316        self
317    }
318
319    /// Search for a certificate with the given public key hash.
320    ///
321    /// This is only compatible with [`ItemClass::certificate`], to search for
322    /// a key by public key hash use [`ItemSearchOptions::application_label`]
323    /// instead.
324    #[inline(always)]
325    pub fn pub_key_hash(&mut self, pub_key_hash: &[u8]) -> &mut Self {
326        self.pub_key_hash = Some(CFData::from_buffer(pub_key_hash));
327        self
328    }
329
330    /// Search for a certificate with the given serial number.
331    ///
332    /// This is only compatible with [`ItemClass::certificate`].
333    #[inline(always)]
334    pub fn serial_number(&mut self, serial_number: &[u8]) -> &mut Self {
335        self.serial_number = Some(CFData::from_buffer(serial_number));
336        self
337    }
338
339    /// Search for a key with the given public key hash.
340    ///
341    /// This is only compatible with [`ItemClass::key`], to search for a
342    /// certificate by the public key hash use [`ItemSearchOptions::pub_key_hash`]
343    /// instead.
344    #[inline(always)]
345    pub fn application_label(&mut self, app_label: &[u8]) -> &mut Self {
346        self.app_label = Some(CFData::from_buffer(app_label));
347        self
348    }
349
350    #[doc(hidden)]
351    #[deprecated(note = "use local_authentication_context")]
352    pub unsafe fn authentication_context(&mut self, authentication_context: *mut std::os::raw::c_void) -> &mut Self {
353        self.authentication_context = unsafe { Some(CFType::wrap_under_create_rule(authentication_context)) };
354        self
355    }
356
357    /// The corresponding value is of type LAContext, and represents a reusable
358    /// local authentication context that should be used for keychain item authentication.
359    #[inline(always)]
360    pub fn local_authentication_context<LAContext: TCFType>(&mut self, authentication_context: Option<LAContext>) -> &mut Self {
361        self.authentication_context = authentication_context.map(|la| la.into_CFType());
362        self
363    }
364
365    /// Whether to skip items in the search that require authentication (default false)
366    #[inline(always)]
367    pub fn skip_authenticated_items(&mut self, do_skip: bool) -> &mut Self {
368        self.skip_authenticated_items = do_skip;
369        self
370    }
371
372    /// Populates a `CFDictionary` to be passed to `update_item` or `delete_item`.
373    // CFDictionary should not be exposed in public Rust APIs.
374    #[inline]
375    fn to_dictionary(&self) -> CFDictionary {
376        unsafe {
377            let mut params = CFMutableDictionary::from_CFType_pairs(&[]);
378
379            if let Some(keychains) = &self.keychains {
380                params.add(
381                    &kSecMatchSearchList.to_void(),
382                    &keychains.as_CFType().to_void(),
383                );
384            } else if self.ignore_legacy_keychains {
385                #[cfg(all(target_os = "macos", feature = "OSX_10_15"))]
386                params.add(
387                    &kSecUseDataProtectionKeychain.to_void(),
388                    &CFBoolean::true_value().to_void(),
389                );
390            }
391
392            if let Some(class) = self.class {
393                params.add(&kSecClass.to_void(), &class.0.to_void());
394            }
395
396            if let Some(case_insensitive) = self.case_insensitive {
397                params.add(
398                    &kSecMatchCaseInsensitive.to_void(),
399                    &CFBoolean::from(case_insensitive).to_void(),
400                );
401            }
402
403            if let Some(key_class) = self.key_class {
404                params.add(&kSecAttrKeyClass.to_void(), &key_class.0.to_void());
405            }
406
407            if self.load_refs {
408                params.add(&kSecReturnRef.to_void(), &CFBoolean::true_value().to_void());
409            }
410
411            if self.load_attributes {
412                params.add(
413                    &kSecReturnAttributes.to_void(),
414                    &CFBoolean::true_value().to_void(),
415                );
416            }
417
418            if self.load_data {
419                params.add(
420                    &kSecReturnData.to_void(),
421                    &CFBoolean::true_value().to_void(),
422                );
423            }
424
425            if let Some(limit) = self.limit {
426                params.add(&kSecMatchLimit.to_void(), &limit.to_value().to_void());
427            }
428
429            if let Some(label) = &self.label {
430                params.add(&kSecAttrLabel.to_void(), &label.to_void());
431            }
432
433            if let Some(trusted_only) = &self.trusted_only {
434                params.add(
435                    &kSecMatchTrustedOnly.to_void(),
436                    &CFBoolean::from(*trusted_only).to_void(),
437                );
438            }
439
440            if let Some(service) = &self.service {
441                params.add(&kSecAttrService.to_void(), &service.to_void());
442            }
443
444            #[cfg(target_os = "macos")]
445            {
446                if let Some(subject) = &self.subject {
447                    params.add(&kSecMatchSubjectWholeString.to_void(), &subject.to_void());
448                }
449            }
450
451            if let Some(account) = &self.account {
452                params.add(&kSecAttrAccount.to_void(), &account.to_void());
453            }
454
455            if let Some(access_group) = &self.access_group {
456                params.add(&kSecAttrAccessGroup.to_void(), &access_group.to_void());
457            }
458
459            if let Some(cloud_sync) = &self.cloud_sync {
460                match cloud_sync {
461                    CloudSync::MatchSyncYes => {
462                        params.add(&kSecAttrSynchronizable.to_void(), &CFBoolean::true_value().to_void());
463                    },
464                    CloudSync::MatchSyncNo => {
465                        params.add(&kSecAttrSynchronizable.to_void(), &CFBoolean::false_value().to_void());
466                    },
467                    CloudSync::MatchSyncAny => {
468                        params.add(&kSecAttrSynchronizable.to_void(), &kSecAttrSynchronizableAny.to_void());
469                    },
470                }
471            }
472
473            if let Some(pub_key_hash) = &self.pub_key_hash {
474                params.add(&kSecAttrPublicKeyHash.to_void(), &pub_key_hash.to_void());
475            }
476
477            if let Some(serial_number) = &self.serial_number {
478                params.add(&kSecAttrSerialNumber.to_void(), &serial_number.to_void());
479            }
480
481            if let Some(app_label) = &self.app_label {
482                params.add(&kSecAttrApplicationLabel.to_void(), &app_label.to_void());
483            }
484
485            if let Some(authentication_context) = &self.authentication_context {
486                params.add(&kSecUseAuthenticationContext.to_void(), &authentication_context.to_void());
487            }
488
489            if self.skip_authenticated_items {
490                params.add(&kSecUseAuthenticationUI.to_void(), &kSecUseAuthenticationUISkip.to_void());
491            }
492
493            params.to_immutable()
494        }
495    }
496
497    /// Search for objects.
498    #[inline]
499    pub fn search(&self) -> Result<Vec<SearchResult>> {
500        unsafe {
501            let params = self.to_dictionary();
502
503            let mut ret = ptr::null();
504            cvt(SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret))?;
505            if ret.is_null() {
506                //  SecItemCopyMatching returns NULL if no load_* was specified,
507                //  causing a segfault.
508                return Ok(vec![]);
509            }
510            let type_id = CFGetTypeID(ret);
511
512            let mut items = vec![];
513
514            if type_id == CFArray::<CFType>::type_id() {
515                let array: CFArray<CFType> = CFArray::wrap_under_create_rule(ret as *mut _);
516                for item in array.iter() {
517                    items.push(get_item(item.as_CFTypeRef()));
518                }
519            } else {
520                items.push(get_item(ret));
521                // This is a bit janky, but get_item uses wrap_under_get_rule
522                // which bumps the refcount but we want create semantics
523                CFRelease(ret);
524            }
525
526            Ok(items)
527        }
528    }
529
530    /// Deletes objects matching the search options.
531    ///
532    /// Translates to `SecItemDelete`.
533    #[inline]
534    pub fn delete(&self) -> Result<()> {
535        cvt(unsafe { SecItemDelete(self.to_dictionary().as_concrete_TypeRef()) })
536    }
537}
538
539unsafe fn get_item(item: CFTypeRef) -> SearchResult { unsafe {
540    let type_id = CFGetTypeID(item);
541
542    if type_id == CFData::type_id() {
543        let data = CFData::wrap_under_get_rule(item as *mut _);
544        let mut buf = Vec::new();
545        buf.extend_from_slice(data.bytes());
546        return SearchResult::Data(buf);
547    }
548
549    if type_id == CFDictionary::<*const u8, *const u8>::type_id() {
550        return SearchResult::Dict(CFDictionary::wrap_under_get_rule(item as *mut _));
551    }
552
553    #[cfg(target_os = "macos")]
554    {
555        use crate::os::macos::keychain_item::SecKeychainItem;
556        if type_id == SecKeychainItem::type_id() {
557            return SearchResult::Ref(Reference::KeychainItem(
558                SecKeychainItem::wrap_under_get_rule(item as *mut _),
559            ));
560        }
561    }
562
563    let reference = if type_id == SecCertificate::type_id() {
564        Reference::Certificate(SecCertificate::wrap_under_get_rule(item as *mut _))
565    } else if type_id == SecKey::type_id() {
566        Reference::Key(SecKey::wrap_under_get_rule(item as *mut _))
567    } else if type_id == SecIdentity::type_id() {
568        Reference::Identity(SecIdentity::wrap_under_get_rule(item as *mut _))
569    } else {
570        panic!("Got bad type from SecItemCopyMatching: {type_id}");
571    };
572
573    SearchResult::Ref(reference)
574} }
575
576/// An enum including all objects whose references can be returned from a search.
577///
578/// Note that generic _Keychain Items_, such as passwords and preferences, do
579/// not have specific object types; they are modeled using dictionaries and so
580/// are available directly as search results in variant `SearchResult::Dict`.
581#[derive(Debug)]
582pub enum Reference {
583    /// A `SecIdentity`.
584    Identity(SecIdentity),
585    /// A `SecCertificate`.
586    Certificate(SecCertificate),
587    /// A `SecKey`.
588    Key(SecKey),
589    /// A `SecKeychainItem`.
590    ///
591    /// Only defined on OSX
592    #[cfg(target_os = "macos")]
593    KeychainItem(crate::os::macos::keychain_item::SecKeychainItem),
594    #[doc(hidden)]
595    __NonExhaustive,
596}
597
598/// An individual search result.
599pub enum SearchResult {
600    /// A reference to the Security Framework object, if asked for.
601    Ref(Reference),
602    /// A dictionary of data about the Security Framework object, if asked for.
603    Dict(CFDictionary),
604    /// The Security Framework object as bytes, if asked for.
605    Data(Vec<u8>),
606    /// An unknown representation of the Security Framework object.
607    Other,
608}
609
610impl fmt::Debug for SearchResult {
611    #[cold]
612    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
613        match self {
614            Self::Ref(reference) => fmt
615                .debug_struct("SearchResult::Ref")
616                .field("reference", reference)
617                .finish(),
618            Self::Data(buf) => fmt
619                .debug_struct("SearchResult::Data")
620                .field("data", buf)
621                .finish(),
622            Self::Dict(_) => {
623                let mut debug = fmt.debug_struct("SearchResult::Dict");
624                for (k, v) in self.simplify_dict().unwrap() {
625                    debug.field(&k, &v);
626                }
627                debug.finish()
628            },
629            Self::Other => write!(fmt, "SearchResult::Other"),
630        }
631    }
632}
633
634impl SearchResult {
635    /// If the search result is a `CFDict`, simplify that to a
636    /// `HashMap<String, String>`. This transformation isn't
637    /// comprehensive, it only supports `CFString`, `CFDate`, and `CFData`
638    /// value types.
639    #[must_use]
640    pub fn simplify_dict(&self) -> Option<HashMap<String, String>> {
641        match self {
642            Self::Dict(d) => unsafe {
643                let mut retmap = HashMap::new();
644                let (keys, values) = d.get_keys_and_values();
645                for (k, v) in keys.iter().zip(values.iter()) {
646                    let keycfstr = CFString::wrap_under_get_rule((*k).cast());
647                    let val: String = match CFGetTypeID(*v) {
648                        cfstring if cfstring == CFString::type_id() => {
649                            format!("{}", CFString::wrap_under_get_rule((*v).cast()))
650                        },
651                        cfdata if cfdata == CFData::type_id() => {
652                            let buf = CFData::wrap_under_get_rule((*v).cast());
653                            let mut vec = Vec::new();
654                            vec.extend_from_slice(buf.bytes());
655                            format!("{}", String::from_utf8_lossy(&vec))
656                        },
657                        cfdate if cfdate == CFDate::type_id() => {
658                            format!("{}", CFString::wrap_under_create_rule(CFCopyDescription(*v)))
659                        },
660                        _ => String::from("unknown"),
661                    };
662                    retmap.insert(format!("{keycfstr}"), val);
663                }
664                Some(retmap)
665            },
666            _ => None,
667        }
668    }
669}
670
671/// Builder-pattern struct for specifying options for `add_item` (`SecAddItem`
672/// wrapper).
673///
674/// When finished populating options call [`Self::add`].
675pub struct ItemAddOptions {
676    /// The value (by ref or data) of the item to add, required.
677    pub value: ItemAddValue,
678    /// Optional kSecAttrAccount attribute.
679    pub account_name: Option<CFString>,
680    /// Optional kSecAttrAccessGroup attribute.
681    pub access_group: Option<CFString>,
682    /// Optional kSecAttrComment attribute.
683    pub comment: Option<CFString>,
684    /// Optional kSecAttrDescription attribute.
685    pub description: Option<CFString>,
686    /// Optional kSecAttrLabel attribute.
687    pub label: Option<CFString>,
688    /// Optional kSecAttrService attribute.
689    pub service: Option<CFString>,
690    /// Optional keychain location.
691    pub location: Option<Location>,
692}
693
694impl ItemAddOptions {
695    /// Specifies the item to add.
696    #[inline]
697    #[must_use]
698    pub fn new(value: ItemAddValue) -> Self {
699        Self {
700            value,
701            label: None,
702            location: None,
703            service: None,
704            account_name: None,
705            comment: None,
706            description: None,
707            access_group: None,
708        }
709    }
710
711    /// Specifies the `kSecAttrAccount` attribute.
712    #[inline]
713    pub fn set_account_name(&mut self, account_name: impl AsRef<str>) -> &mut Self {
714        self.account_name = Some(account_name.as_ref().into());
715        self
716    }
717
718    /// Specifies the `kSecAttrAccessGroup` attribute.
719    #[inline]
720    pub fn set_access_group(&mut self, access_group: impl AsRef<str>) -> &mut Self {
721        self.access_group = Some(access_group.as_ref().into());
722        self
723    }
724
725    /// Specifies the `kSecAttrComment` attribute.
726    #[inline]
727    pub fn set_comment(&mut self, comment: impl AsRef<str>) -> &mut Self {
728        self.comment = Some(comment.as_ref().into());
729        self
730    }
731
732    /// Specifies the `kSecAttrDescription` attribute.
733    #[inline]
734    pub fn set_description(&mut self, description: impl AsRef<str>) -> &mut Self {
735        self.description = Some(description.as_ref().into());
736        self
737    }
738
739    /// Specifies the `kSecAttrLabel` attribute.
740    #[inline]
741    pub fn set_label(&mut self, label: impl AsRef<str>) -> &mut Self {
742        self.label = Some(label.as_ref().into());
743        self
744    }
745
746    /// Specifies which keychain to add the item to.
747    #[inline]
748    pub fn set_location(&mut self, location: Location) -> &mut Self {
749        self.location = Some(location);
750        self
751    }
752
753    /// Specifies the `kSecAttrService` attribute.
754    #[inline]
755    pub fn set_service(&mut self, service: impl AsRef<str>) -> &mut Self {
756        self.service = Some(service.as_ref().into());
757        self
758    }
759
760    /// Populates a `CFDictionary` to be passed to `add_item`.
761    #[deprecated(since = "3.0.0", note = "use `ItemAddOptions::add` instead")]
762    // CFDictionary should not be exposed in public Rust APIs.
763    #[must_use]
764    pub fn to_dictionary(&self) -> CFDictionary {
765        let mut dict = CFMutableDictionary::from_CFType_pairs(&[]);
766
767        let class_opt = match &self.value {
768            ItemAddValue::Ref(ref_) => ref_.class(),
769            ItemAddValue::Data { class, .. } => Some(*class),
770        };
771        if let Some(class) = class_opt {
772            dict.add(&unsafe { kSecClass }.to_void(), &class.0.to_void());
773        }
774
775        let value_pair = match &self.value {
776            ItemAddValue::Ref(ref_) => (unsafe { kSecValueRef }.to_void(), ref_.ref_()),
777            ItemAddValue::Data { data, .. } => (unsafe { kSecValueData }.to_void(), data.to_void()),
778        };
779        dict.add(&value_pair.0, &value_pair.1);
780
781        if let Some(location) = &self.location {
782            match location {
783                #[cfg(any(feature = "OSX_10_15", not(target_os = "macos")))]
784                Location::DataProtectionKeychain => {
785                    dict.add(
786                        &unsafe { kSecUseDataProtectionKeychain }.to_void(),
787                        &CFBoolean::true_value().to_void(),
788                    );
789                }
790                #[cfg(target_os = "macos")]
791                Location::DefaultFileKeychain => {},
792                #[cfg(target_os = "macos")]
793                Location::FileKeychain(keychain) => {
794                    dict.add(&unsafe { kSecUseKeychain }.to_void(), &keychain.to_void());
795                },
796            }
797        }
798        if let Some(account_name) = &self.account_name {
799            dict.add(&unsafe { kSecAttrAccount }.to_void(), &account_name.to_void());
800        }
801        if let Some(access_group) = &self.access_group {
802            dict.add(&unsafe { kSecAttrAccessGroup }.to_void(), &access_group.to_void());
803        }
804        if let Some(comment) = &self.comment {
805            dict.add(&unsafe { kSecAttrComment }.to_void(), &comment.to_void());
806        }
807        if let Some(description) = &self.description {
808            dict.add(&unsafe { kSecAttrDescription }.to_void(), &description.to_void());
809        }
810        if let Some(label) = &self.label {
811            dict.add(&unsafe { kSecAttrLabel }.to_void(), &label.to_void());
812        }
813        if let Some(service) = &self.service {
814            dict.add(&unsafe { kSecAttrService }.to_void(), &service.to_void());
815        }
816
817        dict.to_immutable()
818    }
819
820    /// Adds the item to the keychain.
821    ///
822    /// Translates to `SecItemAdd`.
823    #[inline]
824    pub fn add(&self) -> Result<()> {
825        #[allow(deprecated)]
826        cvt(unsafe { SecItemAdd(self.to_dictionary().as_concrete_TypeRef(), std::ptr::null_mut()) })
827    }
828}
829
830/// Value of an item to add to the keychain.
831pub enum ItemAddValue {
832    /// Pass item by Ref (kSecValueRef)
833    Ref(AddRef),
834    /// Pass item by Data (kSecValueData)
835    Data {
836        /// The item class (kSecClass).
837        class: ItemClass,
838        /// The item data.
839        data: CFData,
840    },
841}
842
843/// Type of Ref to add to the keychain.
844pub enum AddRef {
845    /// `SecKey`
846    Key(SecKey),
847    /// `SecIdentity`
848    Identity(SecIdentity),
849    /// `SecCertificate`
850    Certificate(SecCertificate),
851}
852
853impl AddRef {
854    fn class(&self) -> Option<ItemClass> {
855        match self {
856            Self::Key(_) => Some(ItemClass::key()),
857            //  kSecClass should not be specified when adding a SecIdentityRef:
858            //  https://developer.apple.com/forums/thread/25751
859            Self::Identity(_) => None,
860            Self::Certificate(_) => Some(ItemClass::certificate()),
861        }
862    }
863
864    fn ref_(&self) -> CFTypeRef {
865        match self {
866            Self::Key(key) => key.as_CFTypeRef(),
867            Self::Identity(id) => id.as_CFTypeRef(),
868            Self::Certificate(cert) => cert.as_CFTypeRef(),
869        }
870    }
871}
872
873/// Builder-pattern struct for specifying options for `update_item` (`SecUpdateItem`
874/// wrapper).
875///
876/// When finished populating options call [`update_item`].
877#[derive(Default)]
878pub struct ItemUpdateOptions {
879    /// Optional value (by ref or data) of the item to update.
880    pub value: Option<ItemUpdateValue>,
881    /// Optional kSecAttrAccount attribute.
882    pub account_name: Option<CFString>,
883    /// Optional kSecAttrAccessGroup attribute.
884    pub access_group: Option<CFString>,
885    /// Optional kSecAttrComment attribute.
886    pub comment: Option<CFString>,
887    /// Optional kSecAttrDescription attribute.
888    pub description: Option<CFString>,
889    /// Optional kSecAttrLabel attribute.
890    pub label: Option<CFString>,
891    /// Optional kSecAttrService attribute.
892    pub service: Option<CFString>,
893    /// Optional keychain location.
894    pub location: Option<Location>,
895    /// Optional kSecClass.
896    ///
897    /// Overwrites `value`'s class if set.
898    pub class: Option<ItemClass>,
899}
900
901impl ItemUpdateOptions {
902    /// Specifies the item to add.
903    #[must_use]
904    pub fn new() -> Self {
905        Default::default()
906    }
907
908    /// Specifies the value of the item.
909    #[inline]
910    pub fn set_value(&mut self, value: ItemUpdateValue) -> &mut Self {
911        self.value = Some(value);
912        self
913    }
914
915    /// Specifies the `kSecClass` attribute.
916    #[inline]
917    pub fn set_class(&mut self, class: ItemClass) -> &mut Self {
918        self.class = Some(class);
919        self
920    }
921
922    /// Specifies the `kSecAttrAccount` attribute.
923    #[inline]
924    pub fn set_account_name(&mut self, account_name: impl AsRef<str>) -> &mut Self {
925        self.account_name = Some(account_name.as_ref().into());
926        self
927    }
928
929    /// Specifies the `kSecAttrAccessGroup` attribute.
930    #[inline]
931    pub fn set_access_group(&mut self, access_group: impl AsRef<str>) -> &mut Self {
932        self.access_group = Some(access_group.as_ref().into());
933        self
934    }
935
936    /// Specifies the `kSecAttrComment` attribute.
937    #[inline]
938    pub fn set_comment(&mut self, comment: impl AsRef<str>) -> &mut Self {
939        self.comment = Some(comment.as_ref().into());
940        self
941    }
942
943    /// Specifies the `kSecAttrDescription` attribute.
944    #[inline]
945    pub fn set_description(&mut self, description: impl AsRef<str>) -> &mut Self {
946        self.description = Some(description.as_ref().into());
947        self
948    }
949
950    /// Specifies the `kSecAttrLabel` attribute.
951    #[inline]
952    pub fn set_label(&mut self, label: impl AsRef<str>) -> &mut Self {
953        self.label = Some(label.as_ref().into());
954        self
955    }
956
957    /// Specifies which keychain to add the item to.
958    #[inline]
959    pub fn set_location(&mut self, location: Location) -> &mut Self {
960        self.location = Some(location);
961        self
962    }
963
964    /// Specifies the `kSecAttrService` attribute.
965    #[inline]
966    pub fn set_service(&mut self, service: impl AsRef<str>) -> &mut Self {
967        self.service = Some(service.as_ref().into());
968        self
969    }
970
971    /// Populates a `CFDictionary` to be passed to `update_item`.
972    // CFDictionary should not be exposed in public Rust APIs.
973    #[inline]
974    fn to_dictionary(&self) -> CFDictionary {
975        let mut dict = CFMutableDictionary::from_CFType_pairs(&[]);
976
977        if let Some(value) = &self.value {
978            let class_opt = match value {
979                ItemUpdateValue::Ref(ref_) => ref_.class(),
980                ItemUpdateValue::Data(_) => None,
981            };
982            // `self.class` overwrites `value`'s class if set.
983            if self.class.is_none() {
984                if let Some(class) = class_opt {
985                    dict.add(&unsafe { kSecClass }.to_void(), &class.0.to_void());
986                }
987            }
988            let value_pair = match value {
989                ItemUpdateValue::Ref(ref_) => (unsafe { kSecValueRef }.to_void(), ref_.ref_()),
990                ItemUpdateValue::Data(data) => (unsafe { kSecValueData }.to_void(), data.to_void()),
991            };
992            dict.add(&value_pair.0, &value_pair.1);
993        }
994        if let Some(class) = self.class {
995            dict.add(&unsafe { kSecClass }.to_void(), &class.0.to_void());
996        }
997        if let Some(location) = &self.location {
998            match location {
999                #[cfg(any(feature = "OSX_10_15", not(target_os = "macos")))]
1000                Location::DataProtectionKeychain => {
1001                    dict.add(
1002                        &unsafe { kSecUseDataProtectionKeychain }.to_void(),
1003                        &CFBoolean::true_value().to_void(),
1004                    );
1005                }
1006                #[cfg(target_os = "macos")]
1007                Location::DefaultFileKeychain => {},
1008                #[cfg(target_os = "macos")]
1009                Location::FileKeychain(keychain) => {
1010                    dict.add(&unsafe { kSecUseKeychain }.to_void(), &keychain.to_void());
1011                },
1012            }
1013        }
1014        if let Some(account_name) = &self.account_name {
1015            dict.add(&unsafe { kSecAttrAccount }.to_void(), &account_name.to_void());
1016        }
1017        if let Some(access_group) = &self.access_group {
1018            dict.add(&unsafe { kSecAttrAccessGroup }.to_void(), &access_group.to_void());
1019        }
1020        if let Some(comment) = &self.comment {
1021            dict.add(&unsafe { kSecAttrComment }.to_void(), &comment.to_void());
1022        }
1023        if let Some(description) = &self.description {
1024            dict.add(&unsafe { kSecAttrDescription }.to_void(), &description.to_void());
1025        }
1026        if let Some(label) = &self.label {
1027            dict.add(&unsafe { kSecAttrLabel }.to_void(), &label.to_void());
1028        }
1029        if let Some(service) = &self.service {
1030            dict.add(&unsafe { kSecAttrService }.to_void(), &service.to_void());
1031        }
1032
1033        dict.to_immutable()
1034    }
1035}
1036
1037/// Value of an item to update in the keychain.
1038pub enum ItemUpdateValue {
1039    /// Pass item by Ref (kSecValueRef)
1040    Ref(AddRef),
1041    /// Pass item by Data (kSecValueData)
1042    ///
1043    /// Note that if the [`ItemClass`] of the updated data is different to the original data
1044    /// stored in the keychain, it should be specified using [`ItemUpdateOptions::set_class`].
1045    Data(CFData),
1046}
1047
1048/// Which keychain to add an item to.
1049///
1050/// <https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains>
1051pub enum Location {
1052    /// Store the item in the newer `DataProtectionKeychain`. This is the only
1053    /// keychain on iOS. On macOS, this is the newer and more consistent
1054    /// keychain implementation. Keys stored in the Secure Enclave _must_ use
1055    /// this keychain.
1056    ///
1057    /// This keychain requires the calling binary to be codesigned with
1058    /// entitlements for the `KeychainAccessGroups` it is supposed to
1059    /// access.
1060    #[cfg(any(feature = "OSX_10_15", not(target_os = "macos")))]
1061    DataProtectionKeychain,
1062    /// Store the key in the default file-based keychain. On macOS, defaults to
1063    /// the Login keychain.
1064    #[cfg(target_os = "macos")]
1065    DefaultFileKeychain,
1066    /// Store the key in a specific file-based keychain.
1067    #[cfg(target_os = "macos")]
1068    FileKeychain(crate::os::macos::keychain::SecKeychain),
1069}
1070
1071impl fmt::Debug for Location {
1072    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1073        f.write_str(match self {
1074            #[cfg(any(feature = "OSX_10_15", not(target_os = "macos")))]
1075            Self::DataProtectionKeychain => "DataProtectionKeychain",
1076            #[cfg(target_os = "macos")]
1077            Self::DefaultFileKeychain => "DefaultFileKeychain",
1078            #[cfg(target_os = "macos")]
1079            Self::FileKeychain(_) => "FileKeychain",
1080        })
1081    }
1082}
1083
1084/// Translates to `SecItemAdd`. Use `ItemAddOptions` to build an `add_params`
1085/// `CFDictionary`.
1086#[deprecated(since = "3.0.0", note = "use `ItemAddOptions::add` instead")]
1087#[allow(deprecated)]
1088pub fn add_item(add_params: CFDictionary) -> Result<()> {
1089    cvt(unsafe { SecItemAdd(add_params.as_concrete_TypeRef(), std::ptr::null_mut()) })
1090}
1091
1092/// Translates to `SecItemUpdate`.
1093pub fn update_item(search_params: &ItemSearchOptions, update_params: &ItemUpdateOptions) -> Result<()> {
1094    cvt(unsafe { SecItemUpdate(
1095        search_params.to_dictionary().as_concrete_TypeRef(),
1096        update_params.to_dictionary().as_concrete_TypeRef()
1097    )})
1098}
1099
1100#[cfg(test)]
1101mod test {
1102    use super::*;
1103
1104    #[test]
1105    fn find_nothing() {
1106        assert!(ItemSearchOptions::new().search().is_err());
1107    }
1108
1109    #[test]
1110    fn limit_two() {
1111        let results = ItemSearchOptions::new()
1112            .class(ItemClass::certificate())
1113            .limit(2)
1114            .search()
1115            .unwrap();
1116        assert_eq!(results.len(), 2);
1117    }
1118
1119    #[test]
1120    fn limit_all() {
1121        let results = ItemSearchOptions::new()
1122            .class(ItemClass::certificate())
1123            .limit(Limit::All)
1124            .search()
1125            .unwrap();
1126        assert!(results.len() >= 2);
1127    }
1128}