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)]
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 #[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 #[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 #[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 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 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 let found = find_generic_password(Some(&both_keychains), service, account);
492 assert!(found.is_err());
493
494 keychains1[0]
496 .set_generic_password(service, account, &password)
497 .expect("set_generic_password");
498
499 let (found, item) = find_generic_password(Some(&keychains1), service, account)
501 .expect("find_generic_password1");
502 assert_eq!(found.to_owned(), password);
503
504 let found = find_generic_password(Some(&keychains2), service, account);
506 assert!(found.is_err());
507
508 item.delete();
510
511 temp_keychain_teardown(dir1);
512 temp_keychain_teardown(dir2);
513 }
514}