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