apple_security/os/macos/
passwords.rs

1//! Password support.
2
3use crate::os::macos::keychain::SecKeychain;
4use crate::os::macos::keychain_item::SecKeychainItem;
5use core_foundation::array::CFArray;
6use core_foundation::base::TCFType;
7pub use apple_security_sys::keychain::{SecAuthenticationType, SecProtocolType};
8use apple_security_sys::keychain::{
9    SecKeychainAddGenericPassword, SecKeychainAddInternetPassword, SecKeychainFindGenericPassword,
10    SecKeychainFindInternetPassword,
11};
12use apple_security_sys::keychain_item::{
13    SecKeychainItemDelete, SecKeychainItemFreeContent, SecKeychainItemModifyAttributesAndData,
14};
15use std::fmt;
16use std::fmt::Write;
17use std::ops::Deref;
18use std::ptr;
19use std::slice;
20
21use crate::base::Result;
22use crate::cvt;
23
24/// Password slice. Use `.as_ref()` to get `&[u8]` or `.to_owned()` to get `Vec<u8>`
25pub struct SecKeychainItemPassword {
26    data: *const u8,
27    data_len: usize,
28}
29
30impl fmt::Debug for SecKeychainItemPassword {
31    #[cold]
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        for _ in 0..self.data_len {
34            f.write_char('•')?;
35        }
36        Ok(())
37    }
38}
39
40impl AsRef<[u8]> for SecKeychainItemPassword {
41    #[inline]
42    fn as_ref(&self) -> &[u8] {
43        unsafe { slice::from_raw_parts(self.data, self.data_len) }
44    }
45}
46
47impl Deref for SecKeychainItemPassword {
48    type Target = [u8];
49    #[inline(always)]
50    fn deref(&self) -> &Self::Target {
51        self.as_ref()
52    }
53}
54
55impl Drop for SecKeychainItemPassword {
56    #[inline]
57    fn drop(&mut self) {
58        unsafe {
59            SecKeychainItemFreeContent(ptr::null_mut(), self.data as *mut _);
60        }
61    }
62}
63
64impl SecKeychainItem {
65    /// Modify keychain item in-place, replacing its password with the given one
66    pub fn set_password(&mut self, password: &[u8]) -> Result<()> {
67        unsafe {
68            cvt(SecKeychainItemModifyAttributesAndData(
69                self.as_CFTypeRef() as *mut _,
70                ptr::null(),
71                password.len() as u32,
72                password.as_ptr().cast(),
73            ))?;
74        }
75        Ok(())
76    }
77
78    /// Delete this item from its keychain
79    #[inline]
80    pub fn delete(self) {
81        unsafe {
82            SecKeychainItemDelete(self.as_CFTypeRef() as *mut _);
83        }
84    }
85}
86
87/// Find a generic password.
88///
89/// The underlying system supports passwords with 0 values, so this
90/// returns a vector of bytes rather than a string.
91///
92/// * `keychains` is an array of keychains to search or None to search
93///   the default keychain.
94/// * `service` is the name of the service to search for.
95/// * `account` is the name of the account to search for.
96pub fn find_generic_password(
97    keychains: Option<&[SecKeychain]>,
98    service: &str,
99    account: &str,
100) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
101    let keychains_or_none = keychains.map(CFArray::from_CFTypes);
102
103    let keychains_or_null = match keychains_or_none {
104        None => ptr::null(),
105        Some(ref keychains) => keychains.as_CFTypeRef(),
106    };
107
108    let mut data_len = 0;
109    let mut data = ptr::null_mut();
110    let mut item = ptr::null_mut();
111
112    unsafe {
113        cvt(SecKeychainFindGenericPassword(
114            keychains_or_null,
115            service.len() as u32,
116            service.as_ptr().cast(),
117            account.len() as u32,
118            account.as_ptr().cast(),
119            &mut data_len,
120            &mut data,
121            &mut item,
122        ))?;
123        Ok((
124            SecKeychainItemPassword {
125                data: data as *const _,
126                data_len: data_len as usize,
127            },
128            SecKeychainItem::wrap_under_create_rule(item),
129        ))
130    }
131}
132
133/// * `keychains` is an array of keychains to search or None to search
134///   the default keychain.
135/// * `server`: server name.
136/// * `security_domain`: security domain. This parameter is optional.
137/// * `account`: account name.
138/// * `path`: the path.
139/// * `port`: The TCP/IP port number.
140/// * `protocol`: The protocol associated with this password.
141/// * `authentication_type`: The authentication scheme used.
142#[allow(clippy::too_many_arguments)]
143pub fn find_internet_password(
144    keychains: Option<&[SecKeychain]>,
145    server: &str,
146    security_domain: Option<&str>,
147    account: &str,
148    path: &str,
149    port: Option<u16>,
150    protocol: SecProtocolType,
151    authentication_type: SecAuthenticationType,
152) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
153    let keychains_or_none = keychains.map(CFArray::from_CFTypes);
154
155    let keychains_or_null = match keychains_or_none {
156        None => ptr::null(),
157        Some(ref keychains) => keychains.as_CFTypeRef(),
158    };
159
160    let mut data_len = 0;
161    let mut data = ptr::null_mut();
162    let mut item = ptr::null_mut();
163
164    unsafe {
165        cvt(SecKeychainFindInternetPassword(
166            keychains_or_null,
167            server.len() as u32,
168            server.as_ptr().cast(),
169            security_domain.map_or(0, |s| s.len() as u32),
170            security_domain
171                .map_or(ptr::null(), |s| s.as_ptr().cast()),
172            account.len() as u32,
173            account.as_ptr().cast(),
174            path.len() as u32,
175            path.as_ptr().cast(),
176            port.unwrap_or(0),
177            protocol,
178            authentication_type,
179            &mut data_len,
180            &mut data,
181            &mut item,
182        ))?;
183        Ok((
184            SecKeychainItemPassword {
185                data: data as *const _,
186                data_len: data_len as usize,
187            },
188            SecKeychainItem::wrap_under_create_rule(item),
189        ))
190    }
191}
192
193impl SecKeychain {
194    /// Find application password in this keychain
195    #[inline]
196    pub fn find_generic_password(
197        &self,
198        service: &str,
199        account: &str,
200    ) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
201        find_generic_password(Some(&[self.clone()]), service, account)
202    }
203
204    /// Find internet password in this keychain
205    #[inline]
206    #[allow(clippy::too_many_arguments)]
207    pub fn find_internet_password(
208        &self,
209        server: &str,
210        security_domain: Option<&str>,
211        account: &str,
212        path: &str,
213        port: Option<u16>,
214        protocol: SecProtocolType,
215        authentication_type: SecAuthenticationType,
216    ) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
217        find_internet_password(
218            Some(&[self.clone()]),
219            server,
220            security_domain,
221            account,
222            path,
223            port,
224            protocol,
225            authentication_type,
226        )
227    }
228
229    /// Update existing or add new internet password
230    #[allow(clippy::too_many_arguments)]
231    pub fn set_internet_password(
232        &self,
233        server: &str,
234        security_domain: Option<&str>,
235        account: &str,
236        path: &str,
237        port: Option<u16>,
238        protocol: SecProtocolType,
239        authentication_type: SecAuthenticationType,
240        password: &[u8],
241    ) -> Result<()> {
242        match self.find_internet_password(
243            server,
244            security_domain,
245            account,
246            path,
247            port,
248            protocol,
249            authentication_type,
250        ) {
251            Ok((_, mut item)) => item.set_password(password),
252            _ => self.add_internet_password(
253                server,
254                security_domain,
255                account,
256                path,
257                port,
258                protocol,
259                authentication_type,
260                password,
261            ),
262        }
263    }
264
265    /// Set a generic password.
266    ///
267    /// * `keychain_opt` is the keychain to use or None to use the default
268    ///   keychain.
269    /// * `service` is the associated service name for the password.
270    /// * `account` is the associated account name for the password.
271    /// * `password` is the password itself.
272    pub fn set_generic_password(
273        &self,
274        service: &str,
275        account: &str,
276        password: &[u8],
277    ) -> Result<()> {
278        match self.find_generic_password(service, account) {
279            Ok((_, mut item)) => item.set_password(password),
280            _ => self.add_generic_password(service, account, password),
281        }
282    }
283
284    /// Add application password to the keychain, without checking if it exists already
285    ///
286    /// See `set_generic_password()`
287    #[inline]
288    pub fn add_generic_password(
289        &self,
290        service: &str,
291        account: &str,
292        password: &[u8],
293    ) -> Result<()> {
294        unsafe {
295            cvt(SecKeychainAddGenericPassword(
296                self.as_CFTypeRef() as *mut _,
297                service.len() as u32,
298                service.as_ptr().cast(),
299                account.len() as u32,
300                account.as_ptr().cast(),
301                password.len() as u32,
302                password.as_ptr().cast(),
303                ptr::null_mut(),
304            ))?;
305        }
306        Ok(())
307    }
308
309    /// Add internet password to the keychain, without checking if it exists already
310    ///
311    /// See `set_internet_password()`
312    #[inline]
313    #[allow(clippy::too_many_arguments)]
314    pub fn add_internet_password(
315        &self,
316        server: &str,
317        security_domain: Option<&str>,
318        account: &str,
319        path: &str,
320        port: Option<u16>,
321        protocol: SecProtocolType,
322        authentication_type: SecAuthenticationType,
323        password: &[u8],
324    ) -> Result<()> {
325        unsafe {
326            cvt(SecKeychainAddInternetPassword(
327                self.as_CFTypeRef() as *mut _,
328                server.len() as u32,
329                server.as_ptr().cast(),
330                security_domain.map_or(0, |s| s.len() as u32),
331                security_domain
332                    .map_or(ptr::null(), |s| s.as_ptr().cast()),
333                account.len() as u32,
334                account.as_ptr().cast(),
335                path.len() as u32,
336                path.as_ptr().cast(),
337                port.unwrap_or(0),
338                protocol,
339                authentication_type,
340                password.len() as u32,
341                password.as_ptr().cast(),
342                ptr::null_mut(),
343            ))?;
344        }
345        Ok(())
346    }
347}
348
349#[cfg(test)]
350mod test {
351    use super::*;
352    use crate::os::macos::keychain::{CreateOptions, SecKeychain};
353    use tempfile::tempdir;
354    use tempfile::TempDir;
355
356    fn temp_keychain_setup(name: &str) -> (TempDir, SecKeychain) {
357        let dir = tempdir().expect("TempDir::new");
358        let keychain = CreateOptions::new()
359            .password("foobar")
360            .create(dir.path().join(name.to_string() + ".keychain"))
361            .expect("create keychain");
362
363        (dir, keychain)
364    }
365
366    fn temp_keychain_teardown(dir: TempDir) {
367        dir.close().expect("temp dir close");
368    }
369
370    #[test]
371    fn missing_password_temp() {
372        let (dir, keychain) = temp_keychain_setup("missing_password");
373        let keychains = vec![keychain];
374
375        let service = "temp_this_service_does_not_exist";
376        let account = "this_account_is_bogus";
377        let found = find_generic_password(Some(&keychains), service, account);
378
379        assert!(found.is_err());
380
381        temp_keychain_teardown(dir);
382    }
383
384    #[test]
385    #[cfg(feature = "default_keychain_tests")]
386    fn missing_password_default() {
387        let service = "default_this_service_does_not_exist";
388        let account = "this_account_is_bogus";
389        let found = find_generic_password(None, service, account);
390
391        assert!(found.is_err());
392    }
393
394    #[test]
395    fn round_trip_password_temp() {
396        let (dir, keychain) = temp_keychain_setup("round_trip_password");
397
398        let service = "test_round_trip_password_temp";
399        let account = "temp_this_is_the_test_account";
400        let password = String::from("deadbeef").into_bytes();
401
402        keychain
403            .set_generic_password(service, account, &password)
404            .expect("set_generic_password");
405        let (found, item) = keychain
406            .find_generic_password(service, account)
407            .expect("find_generic_password");
408        assert_eq!(found.to_owned(), password);
409
410        item.delete();
411
412        temp_keychain_teardown(dir);
413    }
414
415    #[test]
416    #[cfg(feature = "default_keychain_tests")]
417    fn round_trip_password_default() {
418        let service = "test_round_trip_password_default";
419        let account = "this_is_the_test_account";
420        let password = String::from("deadbeef").into_bytes();
421
422        SecKeychain::default()
423            .expect("default keychain")
424            .set_generic_password(service, account, &password)
425            .expect("set_generic_password");
426        let (found, item) =
427            find_generic_password(None, service, account).expect("find_generic_password");
428        assert_eq!(&*found, &password[..]);
429
430        item.delete();
431    }
432
433    #[test]
434    fn change_password_temp() {
435        let (dir, keychain) = temp_keychain_setup("change_password");
436        let keychains = vec![keychain];
437
438        let service = "test_change_password_temp";
439        let account = "this_is_the_test_account";
440        let pw1 = String::from("password1").into_bytes();
441        let pw2 = String::from("password2").into_bytes();
442
443        keychains[0]
444            .set_generic_password(service, account, &pw1)
445            .expect("set_generic_password1");
446        let (found, _) = find_generic_password(Some(&keychains), service, account)
447            .expect("find_generic_password1");
448        assert_eq!(found.as_ref(), &pw1[..]);
449
450        keychains[0]
451            .set_generic_password(service, account, &pw2)
452            .expect("set_generic_password2");
453        let (found, item) = find_generic_password(Some(&keychains), service, account)
454            .expect("find_generic_password2");
455        assert_eq!(&*found, &pw2[..]);
456
457        item.delete();
458
459        temp_keychain_teardown(dir);
460    }
461
462    #[test]
463    #[cfg(feature = "default_keychain_tests")]
464    fn change_password_default() {
465        let service = "test_change_password_default";
466        let account = "this_is_the_test_account";
467        let pw1 = String::from("password1").into_bytes();
468        let pw2 = String::from("password2").into_bytes();
469
470        SecKeychain::default()
471            .expect("default keychain")
472            .set_generic_password(service, account, &pw1)
473            .expect("set_generic_password1");
474        let (found, _) =
475            find_generic_password(None, service, account).expect("find_generic_password1");
476        assert_eq!(found.to_owned(), pw1);
477
478        SecKeychain::default()
479            .expect("default keychain")
480            .set_generic_password(service, account, &pw2)
481            .expect("set_generic_password2");
482        let (found, item) =
483            find_generic_password(None, service, account).expect("find_generic_password2");
484        assert_eq!(found.to_owned(), pw2);
485
486        item.delete();
487    }
488
489    #[test]
490    fn cross_keychain_corruption_temp() {
491        let (dir1, keychain1) = temp_keychain_setup("cross_corrupt1");
492        let (dir2, keychain2) = temp_keychain_setup("cross_corrupt2");
493        let keychains1 = vec![keychain1.clone()];
494        let keychains2 = vec![keychain2.clone()];
495        let both_keychains = vec![keychain1, keychain2];
496
497        let service = "temp_this_service_does_not_exist";
498        let account = "this_account_is_bogus";
499        let password = String::from("deadbeef").into_bytes();
500
501        // Make sure this password doesn't exist in either keychain.
502        let found = find_generic_password(Some(&both_keychains), service, account);
503        assert!(found.is_err());
504
505        // Set a password in one keychain.
506        keychains1[0]
507            .set_generic_password(service, account, &password)
508            .expect("set_generic_password");
509
510        // Make sure it's found in that keychain.
511        let (found, item) = find_generic_password(Some(&keychains1), service, account)
512            .expect("find_generic_password1");
513        assert_eq!(found.to_owned(), password);
514
515        // Make sure it's _not_ found in the other keychain.
516        let found = find_generic_password(Some(&keychains2), service, account);
517        assert!(found.is_err());
518
519        // Cleanup.
520        item.delete();
521
522        temp_keychain_teardown(dir1);
523        temp_keychain_teardown(dir2);
524    }
525}