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_concrete_TypeRef(),
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_concrete_TypeRef());
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(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(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 fn default_keychain_test_missing_password_default() {
381 let service = "default_this_service_does_not_exist";
382 let account = "this_account_is_bogus";
383 let found = find_generic_password(None, service, account);
384
385 assert!(found.is_err());
386 }
387
388 #[test]
389 fn round_trip_password_temp() {
390 let (dir, keychain) = temp_keychain_setup("round_trip_password");
391
392 let service = "test_round_trip_password_temp";
393 let account = "temp_this_is_the_test_account";
394 let password = String::from("deadbeef").into_bytes();
395
396 keychain.set_generic_password(service, account, &password).expect("set_generic_password");
397 let (found, item) = keychain.find_generic_password(service, account).expect("find_generic_password");
398 assert_eq!(found.to_owned(), password);
399
400 item.delete();
401
402 temp_keychain_teardown(dir);
403 }
404
405 #[test]
406 fn default_keychain_test_round_trip_password_default() {
407 let service = "test_round_trip_password_default";
408 let account = "this_is_the_test_account";
409 let password = String::from("deadbeef").into_bytes();
410
411 SecKeychain::default()
412 .expect("default keychain")
413 .set_generic_password(service, account, &password)
414 .expect("set_generic_password");
415 let (found, item) = find_generic_password(None, service, account).expect("find_generic_password");
416 assert_eq!(&*found, &password[..]);
417
418 item.delete();
419 }
420
421 #[test]
422 fn change_password_temp() {
423 let (dir, keychain) = temp_keychain_setup("change_password");
424 let keychains = vec![keychain];
425
426 let service = "test_change_password_temp";
427 let account = "this_is_the_test_account";
428 let pw1 = String::from("password1").into_bytes();
429 let pw2 = String::from("password2").into_bytes();
430
431 keychains[0]
432 .set_generic_password(service, account, &pw1)
433 .expect("set_generic_password1");
434 let (found, _) = find_generic_password(Some(&keychains), service, account)
435 .expect("find_generic_password1");
436 assert_eq!(found.as_ref(), &pw1[..]);
437
438 keychains[0]
439 .set_generic_password(service, account, &pw2)
440 .expect("set_generic_password2");
441 let (found, item) = find_generic_password(Some(&keychains), service, account)
442 .expect("find_generic_password2");
443 assert_eq!(&*found, &pw2[..]);
444
445 item.delete();
446
447 temp_keychain_teardown(dir);
448 }
449
450 #[test]
451 fn default_keychain_test_change_password_default() {
452 let service = "test_change_password_default";
453 let account = "this_is_the_test_account";
454 let pw1 = String::from("password1").into_bytes();
455 let pw2 = String::from("password2").into_bytes();
456
457 SecKeychain::default()
458 .expect("default keychain")
459 .set_generic_password(service, account, &pw1)
460 .expect("set_generic_password1");
461 let (found, _) = find_generic_password(None, service, account).expect("find_generic_password1");
462 assert_eq!(found.to_owned(), pw1);
463
464 SecKeychain::default()
465 .expect("default keychain")
466 .set_generic_password(service, account, &pw2)
467 .expect("set_generic_password2");
468 let (found, item) = find_generic_password(None, service, account).expect("find_generic_password2");
469 assert_eq!(found.to_owned(), pw2);
470
471 item.delete();
472 }
473
474 #[test]
475 fn cross_keychain_corruption_temp() {
476 let (dir1, keychain1) = temp_keychain_setup("cross_corrupt1");
477 let (dir2, keychain2) = temp_keychain_setup("cross_corrupt2");
478 let keychains1 = vec![keychain1.clone()];
479 let keychains2 = vec![keychain2.clone()];
480 let both_keychains = vec![keychain1, keychain2];
481
482 let service = "temp_this_service_does_not_exist";
483 let account = "this_account_is_bogus";
484 let password = String::from("deadbeef").into_bytes();
485
486 let found = find_generic_password(Some(&both_keychains), service, account);
488 assert!(found.is_err());
489
490 keychains1[0]
492 .set_generic_password(service, account, &password)
493 .expect("set_generic_password");
494
495 let (found, item) = find_generic_password(Some(&keychains1), service, account)
497 .expect("find_generic_password1");
498 assert_eq!(found.to_owned(), password);
499
500 let found = find_generic_password(Some(&keychains2), service, account);
502 assert!(found.is_err());
503
504 item.delete();
506
507 temp_keychain_teardown(dir1);
508 temp_keychain_teardown(dir2);
509 }
510}