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 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
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
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 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 #[inline]
81 pub fn delete(self) {
82 unsafe {
83 SecKeychainItemDelete(self.as_CFTypeRef() as *mut _);
84 }
85 }
86}
87
88pub 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)]
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(std::slice::from_ref(self)), 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(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 #[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(
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 #[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 #[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 let found = find_generic_password(Some(&both_keychains), service, account);
491 assert!(found.is_err());
492
493 keychains1[0]
495 .set_generic_password(service, account, &password)
496 .expect("set_generic_password");
497
498 let (found, item) = find_generic_password(Some(&keychains1), service, account)
500 .expect("find_generic_password1");
501 assert_eq!(found.to_owned(), password);
502
503 let found = find_generic_password(Some(&keychains2), service, account);
505 assert!(found.is_err());
506
507 item.delete();
509
510 temp_keychain_teardown(dir1);
511 temp_keychain_teardown(dir2);
512 }
513}