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