apple_security_framework/os/macos/
passwords.rs

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