security_framework/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 security_framework_sys::keychain::{SecAuthenticationType, SecProtocolType};
8use security_framework_sys::keychain::{
9    SecKeychainAddGenericPassword, SecKeychainAddInternetPassword, SecKeychainFindGenericPassword,
10    SecKeychainFindInternetPassword,
11};
12use security_framework_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
50    #[inline(always)]
51    fn deref(&self) -> &Self::Target {
52        self.as_ref()
53    }
54}
55
56impl Drop for SecKeychainItemPassword {
57    #[inline]
58    fn drop(&mut self) {
59        unsafe {
60            SecKeychainItemFreeContent(ptr::null_mut(), self.data as *mut _);
61        }
62    }
63}
64
65impl SecKeychainItem {
66    /// Modify keychain item in-place, replacing its password with the given one
67    pub fn set_password(&mut self, password: &[u8]) -> Result<()> {
68        unsafe {
69            cvt(SecKeychainItemModifyAttributesAndData(
70                self.as_CFTypeRef() as *mut _,
71                ptr::null(),
72                password.len() as u32,
73                password.as_ptr().cast(),
74            ))?;
75        }
76        Ok(())
77    }
78
79    /// Delete this item from its keychain
80    #[inline]
81    pub fn delete(self) {
82        unsafe {
83            SecKeychainItemDelete(self.as_CFTypeRef() as *mut _);
84        }
85    }
86}
87
88/// Find a generic password.
89///
90/// The underlying system supports passwords with 0 values, so this
91/// returns a vector of bytes rather than a string.
92///
93/// * `keychains` is an array of keychains to search or None to search 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 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(std::slice::from_ref(self)), 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(std::slice::from_ref(self)),
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 keychain.
266    /// * `service` is the associated service name for the password.
267    /// * `account` is the associated account name for the password.
268    /// * `password` is the password itself.
269    pub fn set_generic_password(
270        &self,
271        service: &str,
272        account: &str,
273        password: &[u8],
274    ) -> Result<()> {
275        match self.find_generic_password(service, account) {
276            Ok((_, mut item)) => item.set_password(password),
277            _ => self.add_generic_password(service, account, password),
278        }
279    }
280
281    /// Add application password to the keychain, without checking if it exists already
282    ///
283    /// See `set_generic_password()`
284    #[inline]
285    pub fn add_generic_password(
286        &self,
287        service: &str,
288        account: &str,
289        password: &[u8],
290    ) -> Result<()> {
291        unsafe {
292            cvt(SecKeychainAddGenericPassword(
293                self.as_CFTypeRef() as *mut _,
294                service.len() as u32,
295                service.as_ptr().cast(),
296                account.len() as u32,
297                account.as_ptr().cast(),
298                password.len() as u32,
299                password.as_ptr().cast(),
300                ptr::null_mut(),
301            ))?;
302        }
303        Ok(())
304    }
305
306    /// Add internet password to the keychain, without checking if it exists already
307    ///
308    /// See `set_internet_password()`
309    #[inline]
310    #[allow(clippy::too_many_arguments)]
311    pub fn add_internet_password(
312        &self,
313        server: &str,
314        security_domain: Option<&str>,
315        account: &str,
316        path: &str,
317        port: Option<u16>,
318        protocol: SecProtocolType,
319        authentication_type: SecAuthenticationType,
320        password: &[u8],
321    ) -> Result<()> {
322        unsafe {
323            cvt(SecKeychainAddInternetPassword(
324                self.as_CFTypeRef() as *mut _,
325                server.len() as u32,
326                server.as_ptr().cast(),
327                security_domain.map_or(0, |s| s.len() as u32),
328                security_domain.map_or(ptr::null(), |s| s.as_ptr().cast()),
329                account.len() as u32,
330                account.as_ptr().cast(),
331                path.len() as u32,
332                path.as_ptr().cast(),
333                port.unwrap_or(0),
334                protocol,
335                authentication_type,
336                password.len() as u32,
337                password.as_ptr().cast(),
338                ptr::null_mut(),
339            ))?;
340        }
341        Ok(())
342    }
343}
344
345#[cfg(test)]
346mod test {
347    use super::*;
348    use crate::os::macos::keychain::CreateOptions;
349    use tempfile::{tempdir, TempDir};
350
351    fn temp_keychain_setup(name: &str) -> (TempDir, SecKeychain) {
352        let dir = tempdir().expect("TempDir::new");
353        let keychain = CreateOptions::new()
354            .password("foobar")
355            .create(dir.path().join(name.to_string() + ".keychain"))
356            .expect("create keychain");
357
358        (dir, keychain)
359    }
360
361    fn temp_keychain_teardown(dir: TempDir) {
362        dir.close().expect("temp dir close");
363    }
364
365    #[test]
366    fn missing_password_temp() {
367        let (dir, keychain) = temp_keychain_setup("missing_password");
368        let keychains = vec![keychain];
369
370        let service = "temp_this_service_does_not_exist";
371        let account = "this_account_is_bogus";
372        let found = find_generic_password(Some(&keychains), service, account);
373
374        assert!(found.is_err());
375
376        temp_keychain_teardown(dir);
377    }
378
379    #[test]
380    #[ignore]
381    fn default_keychain_test_missing_password_default() {
382        let service = "default_this_service_does_not_exist";
383        let account = "this_account_is_bogus";
384        let found = find_generic_password(None, service, account);
385
386        assert!(found.is_err());
387    }
388
389    #[test]
390    fn round_trip_password_temp() {
391        let (dir, keychain) = temp_keychain_setup("round_trip_password");
392
393        let service = "test_round_trip_password_temp";
394        let account = "temp_this_is_the_test_account";
395        let password = String::from("deadbeef").into_bytes();
396
397        keychain.set_generic_password(service, account, &password).expect("set_generic_password");
398        let (found, item) = keychain.find_generic_password(service, account).expect("find_generic_password");
399        assert_eq!(found.to_owned(), password);
400
401        item.delete();
402
403        temp_keychain_teardown(dir);
404    }
405
406    #[test]
407    #[ignore]
408    fn default_keychain_test_round_trip_password_default() {
409        let service = "test_round_trip_password_default";
410        let account = "this_is_the_test_account";
411        let password = String::from("deadbeef").into_bytes();
412
413        SecKeychain::default()
414            .expect("default keychain")
415            .set_generic_password(service, account, &password)
416            .expect("set_generic_password");
417        let (found, item) = find_generic_password(None, service, account).expect("find_generic_password");
418        assert_eq!(&*found, &password[..]);
419
420        item.delete();
421    }
422
423    #[test]
424    fn change_password_temp() {
425        let (dir, keychain) = temp_keychain_setup("change_password");
426        let keychains = vec![keychain];
427
428        let service = "test_change_password_temp";
429        let account = "this_is_the_test_account";
430        let pw1 = String::from("password1").into_bytes();
431        let pw2 = String::from("password2").into_bytes();
432
433        keychains[0]
434            .set_generic_password(service, account, &pw1)
435            .expect("set_generic_password1");
436        let (found, _) = find_generic_password(Some(&keychains), service, account)
437            .expect("find_generic_password1");
438        assert_eq!(found.as_ref(), &pw1[..]);
439
440        keychains[0]
441            .set_generic_password(service, account, &pw2)
442            .expect("set_generic_password2");
443        let (found, item) = find_generic_password(Some(&keychains), service, account)
444            .expect("find_generic_password2");
445        assert_eq!(&*found, &pw2[..]);
446
447        item.delete();
448
449        temp_keychain_teardown(dir);
450    }
451
452    #[test]
453    #[ignore]
454    fn default_keychain_test_change_password_default() {
455        let service = "test_change_password_default";
456        let account = "this_is_the_test_account";
457        let pw1 = String::from("password1").into_bytes();
458        let pw2 = String::from("password2").into_bytes();
459
460        SecKeychain::default()
461            .expect("default keychain")
462            .set_generic_password(service, account, &pw1)
463            .expect("set_generic_password1");
464        let (found, _) = find_generic_password(None, service, account).expect("find_generic_password1");
465        assert_eq!(found.to_owned(), pw1);
466
467        SecKeychain::default()
468            .expect("default keychain")
469            .set_generic_password(service, account, &pw2)
470            .expect("set_generic_password2");
471        let (found, item) = find_generic_password(None, service, account).expect("find_generic_password2");
472        assert_eq!(found.to_owned(), pw2);
473
474        item.delete();
475    }
476
477    #[test]
478    fn cross_keychain_corruption_temp() {
479        let (dir1, keychain1) = temp_keychain_setup("cross_corrupt1");
480        let (dir2, keychain2) = temp_keychain_setup("cross_corrupt2");
481        let keychains1 = vec![keychain1.clone()];
482        let keychains2 = vec![keychain2.clone()];
483        let both_keychains = vec![keychain1, keychain2];
484
485        let service = "temp_this_service_does_not_exist";
486        let account = "this_account_is_bogus";
487        let password = String::from("deadbeef").into_bytes();
488
489        // Make sure this password doesn't exist in either keychain.
490        let found = find_generic_password(Some(&both_keychains), service, account);
491        assert!(found.is_err());
492
493        // Set a password in one keychain.
494        keychains1[0]
495            .set_generic_password(service, account, &password)
496            .expect("set_generic_password");
497
498        // Make sure it's found in that keychain.
499        let (found, item) = find_generic_password(Some(&keychains1), service, account)
500            .expect("find_generic_password1");
501        assert_eq!(found.to_owned(), password);
502
503        // Make sure it's _not_ found in the other keychain.
504        let found = find_generic_password(Some(&keychains2), service, account);
505        assert!(found.is_err());
506
507        // Cleanup.
508        item.delete();
509
510        temp_keychain_teardown(dir1);
511        temp_keychain_teardown(dir2);
512    }
513}