1use 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
24pub 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 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 #[inline]
80 pub fn delete(self) {
81 unsafe {
82 SecKeychainItemDelete(self.as_CFTypeRef() as *mut _);
83 }
84 }
85}
86
87pub 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#[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 #[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 #[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 #[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 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 #[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 #[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 let found = find_generic_password(Some(&both_keychains), service, account);
503 assert!(found.is_err());
504
505 keychains1[0]
507 .set_generic_password(service, account, &password)
508 .expect("set_generic_password");
509
510 let (found, item) = find_generic_password(Some(&keychains1), service, account)
512 .expect("find_generic_password1");
513 assert_eq!(found.to_owned(), password);
514
515 let found = find_generic_password(Some(&keychains2), service, account);
517 assert!(found.is_err());
518
519 item.delete();
521
522 temp_keychain_teardown(dir1);
523 temp_keychain_teardown(dir2);
524 }
525}