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
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.map_or(ptr::null(), |s| s.as_ptr().cast()),
171            account.len() as u32,
172            account.as_ptr().cast(),
173            path.len() as u32,
174            path.as_ptr().cast(),
175            port.unwrap_or(0),
176            protocol,
177            authentication_type,
178            &mut data_len,
179            &mut data,
180            &mut item,
181        ))?;
182        Ok((
183            SecKeychainItemPassword {
184                data: data as *const _,
185                data_len: data_len as usize,
186            },
187            SecKeychainItem::wrap_under_create_rule(item),
188        ))
189    }
190}
191
192impl SecKeychain {
193    /// Find application password in this keychain
194    #[inline]
195    pub fn find_generic_password(
196        &self,
197        service: &str,
198        account: &str,
199    ) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
200        find_generic_password(Some(std::slice::from_ref(self)), service, account)
201    }
202
203    /// Find internet password in this keychain
204    #[inline]
205    #[allow(clippy::too_many_arguments)]
206    pub fn find_internet_password(
207        &self,
208        server: &str,
209        security_domain: Option<&str>,
210        account: &str,
211        path: &str,
212        port: Option<u16>,
213        protocol: SecProtocolType,
214        authentication_type: SecAuthenticationType,
215    ) -> Result<(SecKeychainItemPassword, SecKeychainItem)> {
216        find_internet_password(
217            Some(std::slice::from_ref(self)),
218            server,
219            security_domain,
220            account,
221            path,
222            port,
223            protocol,
224            authentication_type,
225        )
226    }
227
228    /// Update existing or add new internet password
229    #[allow(clippy::too_many_arguments)]
230    pub fn set_internet_password(
231        &self,
232        server: &str,
233        security_domain: Option<&str>,
234        account: &str,
235        path: &str,
236        port: Option<u16>,
237        protocol: SecProtocolType,
238        authentication_type: SecAuthenticationType,
239        password: &[u8],
240    ) -> Result<()> {
241        match self.find_internet_password(
242            server,
243            security_domain,
244            account,
245            path,
246            port,
247            protocol,
248            authentication_type,
249        ) {
250            Ok((_, mut item)) => item.set_password(password),
251            _ => self.add_internet_password(
252                server,
253                security_domain,
254                account,
255                path,
256                port,
257                protocol,
258                authentication_type,
259                password,
260            ),
261        }
262    }
263
264    /// Set a generic password.
265    ///
266    /// * `keychain_opt` is the keychain to use or None to use the default 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 super::*;
349    use crate::os::macos::keychain::CreateOptions;
350    use tempfile::{tempdir, TempDir};
351
352    fn temp_keychain_setup(name: &str) -> (TempDir, SecKeychain) {
353        let dir = tempdir().expect("TempDir::new");
354        let keychain = CreateOptions::new()
355            .password("foobar")
356            .create(dir.path().join(name.to_string() + ".keychain"))
357            .expect("create keychain");
358
359        (dir, keychain)
360    }
361
362    fn temp_keychain_teardown(dir: TempDir) {
363        dir.close().expect("temp dir close");
364    }
365
366    #[test]
367    fn missing_password_temp() {
368        let (dir, keychain) = temp_keychain_setup("missing_password");
369        let keychains = vec![keychain];
370
371        let service = "temp_this_service_does_not_exist";
372        let account = "this_account_is_bogus";
373        let found = find_generic_password(Some(&keychains), service, account);
374
375        assert!(found.is_err());
376
377        temp_keychain_teardown(dir);
378    }
379
380    #[test]
381    #[ignore]
382    fn default_keychain_test_missing_password_default() {
383        let service = "default_this_service_does_not_exist";
384        let account = "this_account_is_bogus";
385        let found = find_generic_password(None, service, account);
386
387        assert!(found.is_err());
388    }
389
390    #[test]
391    fn round_trip_password_temp() {
392        let (dir, keychain) = temp_keychain_setup("round_trip_password");
393
394        let service = "test_round_trip_password_temp";
395        let account = "temp_this_is_the_test_account";
396        let password = String::from("deadbeef").into_bytes();
397
398        keychain.set_generic_password(service, account, &password).expect("set_generic_password");
399        let (found, item) = keychain.find_generic_password(service, account).expect("find_generic_password");
400        assert_eq!(found.to_owned(), password);
401
402        item.delete();
403
404        temp_keychain_teardown(dir);
405    }
406
407    #[test]
408    #[ignore]
409    fn default_keychain_test_round_trip_password_default() {
410        let service = "test_round_trip_password_default";
411        let account = "this_is_the_test_account";
412        let password = String::from("deadbeef").into_bytes();
413
414        SecKeychain::default()
415            .expect("default keychain")
416            .set_generic_password(service, account, &password)
417            .expect("set_generic_password");
418        let (found, item) = find_generic_password(None, service, account).expect("find_generic_password");
419        assert_eq!(&*found, &password[..]);
420
421        item.delete();
422    }
423
424    #[test]
425    fn change_password_temp() {
426        let (dir, keychain) = temp_keychain_setup("change_password");
427        let keychains = vec![keychain];
428
429        let service = "test_change_password_temp";
430        let account = "this_is_the_test_account";
431        let pw1 = String::from("password1").into_bytes();
432        let pw2 = String::from("password2").into_bytes();
433
434        keychains[0]
435            .set_generic_password(service, account, &pw1)
436            .expect("set_generic_password1");
437        let (found, _) = find_generic_password(Some(&keychains), service, account)
438            .expect("find_generic_password1");
439        assert_eq!(found.as_ref(), &pw1[..]);
440
441        keychains[0]
442            .set_generic_password(service, account, &pw2)
443            .expect("set_generic_password2");
444        let (found, item) = find_generic_password(Some(&keychains), service, account)
445            .expect("find_generic_password2");
446        assert_eq!(&*found, &pw2[..]);
447
448        item.delete();
449
450        temp_keychain_teardown(dir);
451    }
452
453    #[test]
454    #[ignore]
455    fn default_keychain_test_change_password_default() {
456        let service = "test_change_password_default";
457        let account = "this_is_the_test_account";
458        let pw1 = String::from("password1").into_bytes();
459        let pw2 = String::from("password2").into_bytes();
460
461        SecKeychain::default()
462            .expect("default keychain")
463            .set_generic_password(service, account, &pw1)
464            .expect("set_generic_password1");
465        let (found, _) = find_generic_password(None, service, account).expect("find_generic_password1");
466        assert_eq!(found.to_owned(), pw1);
467
468        SecKeychain::default()
469            .expect("default keychain")
470            .set_generic_password(service, account, &pw2)
471            .expect("set_generic_password2");
472        let (found, item) = find_generic_password(None, service, account).expect("find_generic_password2");
473        assert_eq!(found.to_owned(), pw2);
474
475        item.delete();
476    }
477
478    #[test]
479    fn cross_keychain_corruption_temp() {
480        let (dir1, keychain1) = temp_keychain_setup("cross_corrupt1");
481        let (dir2, keychain2) = temp_keychain_setup("cross_corrupt2");
482        let keychains1 = vec![keychain1.clone()];
483        let keychains2 = vec![keychain2.clone()];
484        let both_keychains = vec![keychain1, keychain2];
485
486        let service = "temp_this_service_does_not_exist";
487        let account = "this_account_is_bogus";
488        let password = String::from("deadbeef").into_bytes();
489
490        // Make sure this password doesn't exist in either keychain.
491        let found = find_generic_password(Some(&both_keychains), service, account);
492        assert!(found.is_err());
493
494        // Set a password in one keychain.
495        keychains1[0]
496            .set_generic_password(service, account, &password)
497            .expect("set_generic_password");
498
499        // Make sure it's found in that keychain.
500        let (found, item) = find_generic_password(Some(&keychains1), service, account)
501            .expect("find_generic_password1");
502        assert_eq!(found.to_owned(), password);
503
504        // Make sure it's _not_ found in the other keychain.
505        let found = find_generic_password(Some(&keychains2), service, account);
506        assert!(found.is_err());
507
508        // Cleanup.
509        item.delete();
510
511        temp_keychain_teardown(dir1);
512        temp_keychain_teardown(dir2);
513    }
514}