Skip to main content

apple_native_keyring_store/
keychain.rs

1/*!
2
3# macOS Keychain credential store
4
5macOS provides file-based secure stores called _keychains_. The OS
6automatically creates three of them (or four if removable media is being used),
7called _User_ (aka login), _Common_, _System_, and _Dynamic_.  The `keychain`
8configuration key specified when instantiating a [Store] determines
9which keychain this store uses for its credentials. By default,
10the 'User' (aka login) keychain is used.
11
12For a given service/user pair, this module creates/searches for a generic
13credential in the store's keychain whose _account_ attribute holds the user
14and whose _service_ attribute holds the service. Because generic credentials are
15uniquely identified within each keychain by their _account_ and _service_
16attributes, there is no chance of ambiguity.
17
18Because of a quirk in the Mac keychain services API, neither the _account_
19nor the _service_ may be the empty string.
20
21In the _Keychain Access_ UI on Mac, credentials created by this module show up
22in the _Passwords_ view (with their _where_ and _name_ fields both showing
23their _service_ attribute). What the Keychain Access lists under _Note_ entries
24on the Mac are also generic credentials, so this module can access existing
25_notes_ created by third-party applications if you know the value of their
26_account_ attribute (which is not displayed by _Keychain Access_).
27
28## Attributes
29
30Credentials on macOS have some fixed _key/value_ attributes, but this
31module ignores all of them.
32
33## Search
34
35You can search the credentials in a given store (keychain) by `service`
36and `user`. The search is case-sensitive, and a wrapper around each
37matching credential is returned. Specifying neither `service` nor `user`
38returns wrappers around all the credentials in the store.
39
40 */
41use std::collections::HashMap;
42use std::sync::Arc;
43use std::time::{SystemTime, UNIX_EPOCH};
44
45use security_framework::base::Error;
46use security_framework::item;
47use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
48use security_framework::os::macos::passwords::find_generic_password;
49
50use keyring_core::{
51    Entry,
52    api::{Credential, CredentialApi, CredentialPersistence, CredentialStoreApi},
53    attributes::parse_attributes,
54    error::{Error as ErrorCode, Result},
55};
56
57/// The representation of a generic Keychain credential.
58///
59/// The actual credentials can have lots of attributes
60/// not represented here.  There's no way to use this
61/// module to get at those attributes.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Cred {
64    pub domain: MacKeychainDomain,
65    pub service: String,
66    pub account: String,
67}
68
69impl CredentialApi for Cred {
70    /// See the keychain-core API docs.
71    fn set_secret(&self, secret: &[u8]) -> Result<()> {
72        self.get_keychain()?
73            .set_generic_password(&self.service, &self.account, secret)
74            .map_err(decode_error)?;
75        Ok(())
76    }
77
78    /// See the keychain-core API docs.
79    fn get_secret(&self) -> Result<Vec<u8>> {
80        let (password_bytes, _) =
81            find_generic_password(Some(&[self.get_keychain()?]), &self.service, &self.account)
82                .map_err(decode_error)?;
83        Ok(password_bytes.to_owned())
84    }
85
86    /// See the keychain-core API docs.
87    fn delete_credential(&self) -> Result<()> {
88        let keychains = [self.get_keychain()?];
89        let mut options = item::ItemSearchOptions::new();
90        options
91            .keychains(&keychains)
92            .class(item::ItemClass::generic_password())
93            .service(&self.service)
94            .account(&self.account);
95        options.delete().map_err(decode_error)
96    }
97
98    /// See the keychain-core API docs.
99    ///
100    /// Since every specifier is also a wrapper, this is just a check
101    /// to see whether the underlying credential exists.
102    fn get_credential(&self) -> Result<Option<Arc<Credential>>> {
103        find_generic_password(Some(&[self.get_keychain()?]), &self.service, &self.account)
104            .map_err(decode_error)?;
105        Ok(None)
106    }
107
108    /// See the keychain-core API docs.
109    fn get_specifiers(&self) -> Option<(String, String)> {
110        Some((self.service.clone(), self.account.clone()))
111    }
112
113    /// See the keychain-core API docs.
114    fn as_any(&self) -> &dyn std::any::Any {
115        self
116    }
117
118    /// See the keychain-core API docs.
119    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        std::fmt::Debug::fmt(self, f)
121    }
122}
123
124impl Cred {
125    /// Create a credential representing a Mac keychain entry.
126    ///
127    /// A keychain string is interpreted as the keychain to use for the entry.
128    ///
129    /// Creating a credential does not put anything into the keychain.
130    /// The keychain entry will be created
131    /// when [set_password](Cred::set_password) is
132    /// called.
133    ///
134    /// This will fail if the service or user strings are empty,
135    /// because empty attribute values act as wildcards in the
136    /// Keychain Services API.
137    pub fn build(keychain: MacKeychainDomain, service: &str, user: &str) -> Result<Entry> {
138        if service.is_empty() {
139            return Err(ErrorCode::Invalid(
140                "service".to_string(),
141                "cannot be empty".to_string(),
142            ));
143        }
144        if user.is_empty() {
145            return Err(ErrorCode::Invalid(
146                "user".to_string(),
147                "cannot be empty".to_string(),
148            ));
149        }
150        let cred = Cred {
151            domain: keychain,
152            service: service.to_string(),
153            account: user.to_string(),
154        };
155        Ok(Entry::new_with_credential(Arc::new(cred)))
156    }
157
158    fn get_keychain(&self) -> Result<SecKeychain> {
159        get_keychain(&self.domain)
160    }
161}
162
163/// The store for Mac keychain credentials
164pub struct Store {
165    id: String,
166    keychain: MacKeychainDomain,
167}
168
169impl std::fmt::Debug for Store {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("Store")
172            .field("vendor", &self.vendor())
173            .field("id", &self.id())
174            .field("domain", &self.keychain)
175            .finish()
176    }
177}
178
179impl Store {
180    /// Create a default store, which uses the User (aka login) keychain.
181    pub fn new() -> Result<Arc<Self>> {
182        Ok(Self::new_internal(MacKeychainDomain::User))
183    }
184
185    /// Create a store configured to use a specific keychain.
186    ///
187    /// The keychain used can be overridden by a modifier on a specific entry.
188    pub fn new_with_configuration(configuration: &HashMap<&str, &str>) -> Result<Arc<Self>> {
189        let config = parse_attributes(&["keychain"], Some(configuration))?;
190        let mut keychain = MacKeychainDomain::User;
191        if let Some(option) = config.get("keychain") {
192            keychain = option.parse()?;
193        }
194        Ok(Self::new_internal(keychain))
195    }
196
197    fn new_internal(keychain: MacKeychainDomain) -> Arc<Self> {
198        let now = SystemTime::now();
199        let elapsed = if now.lt(&UNIX_EPOCH) {
200            UNIX_EPOCH.duration_since(now).unwrap()
201        } else {
202            now.duration_since(UNIX_EPOCH).unwrap()
203        };
204        Arc::new(Store {
205            id: format!(
206                "Keychain Storage, Crate version {}, Instantiated at {}",
207                env!("CARGO_PKG_VERSION"),
208                elapsed.as_secs_f64()
209            ),
210            keychain,
211        })
212    }
213}
214
215impl CredentialStoreApi for Store {
216    /// See the keychain-core API docs.
217    fn vendor(&self) -> String {
218        "macOS Keychain Store, https://crates.io/crates/apple-native-keyring-store".to_string()
219    }
220
221    /// See the keychain-core API docs.
222    fn id(&self) -> String {
223        self.id.to_string()
224    }
225
226    /// See the keychain-core API docs.
227    ///
228    /// The only option you can specify is `keychain`, and the value
229    /// must name a keychain (User, System, Common, or Dynamic)
230    /// you want to use to hold the credential when it's created.
231    /// The default is the User (aka login) keychain.
232    fn build(
233        &self,
234        service: &str,
235        user: &str,
236        modifiers: Option<&HashMap<&str, &str>>,
237    ) -> Result<Entry> {
238        let mods = parse_attributes(&["keychain"], modifiers)?;
239        let mut keychain = self.keychain.clone();
240        if let Some(option) = mods.get("keychain") {
241            keychain = option.parse()?;
242        }
243        Cred::build(keychain, service, user)
244    }
245
246    /// See the keychain-core API docs.
247    ///
248    /// The (optional) search spec keys allowed are `service` and `user`. They
249    /// are matched case-sensitively against the service and account attributes
250    /// of the generic passwords in the store's configured keychain. A wrapper
251    /// for each matching credential is returned. If no `service` or `user` is
252    /// specified, all credentials in the store's configured keychain are
253    /// returned.
254    fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
255        let spec = parse_attributes(&["service", "user"], Some(spec))?;
256        let keychains = [get_keychain(&self.keychain)?];
257        let mut options = item::ItemSearchOptions::new();
258        options
259            .keychains(&keychains)
260            .class(item::ItemClass::generic_password())
261            .limit(item::Limit::All)
262            .load_attributes(true);
263        if let Some(service) = spec.get("service") {
264            options.service(service);
265        }
266        if let Some(user) = spec.get("user") {
267            options.account(user);
268        }
269        let items = match options.search().map_err(decode_error) {
270            Ok(items) => items,
271            Err(ErrorCode::NoEntry) => return Ok(Vec::new()),
272            Err(e) => return Err(e),
273        };
274        let mut result = Vec::new();
275        for item in items {
276            if let Some(map) = item.simplify_dict() {
277                if let Some(service) = map.get("svce") {
278                    if let Some(account) = map.get("acct") {
279                        let cred = Cred {
280                            domain: self.keychain.clone(),
281                            service: service.to_string(),
282                            account: account.to_string(),
283                        };
284                        result.push(Entry::new_with_credential(Arc::new(cred)))
285                    }
286                }
287            }
288        }
289        Ok(result)
290    }
291
292    /// Return the underlying builder object with an `Any` type so that it can
293    /// be downgraded to a [Store] for platform-specific processing.
294    fn as_any(&self) -> &dyn std::any::Any {
295        self
296    }
297
298    //// See the keychain-core API docs.
299    fn persistence(&self) -> CredentialPersistence {
300        CredentialPersistence::UntilDelete
301    }
302
303    /// See the keychain-core API docs.
304    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        std::fmt::Debug::fmt(self, f)
306    }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310/// The four pre-defined Mac keychains.
311pub enum MacKeychainDomain {
312    User,
313    System,
314    Common,
315    Dynamic,
316}
317
318impl std::fmt::Display for MacKeychainDomain {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        match self {
321            MacKeychainDomain::User => "User".fmt(f),
322            MacKeychainDomain::System => "System".fmt(f),
323            MacKeychainDomain::Common => "Common".fmt(f),
324            MacKeychainDomain::Dynamic => "Dynamic".fmt(f),
325        }
326    }
327}
328
329impl std::str::FromStr for MacKeychainDomain {
330    type Err = ErrorCode;
331
332    /// Convert a keychain specification string to a keychain domain.
333    ///
334    /// We accept any case in the string,
335    /// but the value has to match a known keychain domain name.
336    fn from_str(s: &str) -> Result<Self> {
337        match s.to_ascii_lowercase().as_str() {
338            "user" => Ok(MacKeychainDomain::User),
339            "system" => Ok(MacKeychainDomain::System),
340            "common" => Ok(MacKeychainDomain::Common),
341            "dynamic" => Ok(MacKeychainDomain::Dynamic),
342            _ => Err(ErrorCode::Invalid(
343                "keychain".to_string(),
344                format!("'{s}' is not User, System, Common, or Dynamic"),
345            )),
346        }
347    }
348}
349
350fn get_keychain(domain: &MacKeychainDomain) -> Result<SecKeychain> {
351    let domain = match domain {
352        MacKeychainDomain::User => SecPreferencesDomain::User,
353        MacKeychainDomain::System => SecPreferencesDomain::System,
354        MacKeychainDomain::Common => SecPreferencesDomain::Common,
355        MacKeychainDomain::Dynamic => SecPreferencesDomain::Dynamic,
356    };
357    match SecKeychain::default_for_domain(domain) {
358        Ok(keychain) => Ok(keychain),
359        Err(err) => Err(decode_error(err)),
360    }
361}
362
363/// Map a Mac API error to a crate error with appropriate annotation
364///
365/// The macOS error code values used here are from
366/// [this reference](https://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-78/lib/SecBase.h.auto.html)
367pub fn decode_error(err: Error) -> ErrorCode {
368    match err.code() {
369        -61 => ErrorCode::NoStorageAccess(Box::new(err)), // Write permissions error
370        -25244 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecInvalidOwnerEdit
371        -25291 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecNotAvailable
372        -25292 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecReadOnly
373        -25294 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecNoSuchKeychain
374        -25295 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecInvalidKeychain
375        -25300 => ErrorCode::NoEntry,                     // errSecItemNotFound
376        _ => ErrorCode::PlatformFailure(Box::new(err)),
377    }
378}