1use core::ffi::c_void;
4use std::collections::BTreeMap;
5use std::ptr;
6
7use crate::ffi;
8use crate::la_credential::{LACredential, LACredentialType};
9use crate::la_error::{from_status, LAError, Result};
10use crate::la_policy::LAPolicy;
11use crate::private::{
12 bridge_bool, bridge_f64, bridge_i32, bridge_i32_vec, bridge_opt_bytes, bridge_opt_string,
13 bridge_ptr, bridge_string, bridge_unit, cstring, framework_bool_result, OwnedHandle,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[non_exhaustive]
19pub enum BiometryType {
20 None,
21 TouchId,
22 FaceId,
23 OpticId,
24 Unknown(i32),
25}
26
27impl BiometryType {
28 #[must_use]
29 pub const fn from_ffi(value: i32) -> Self {
30 match value {
31 ffi::biometry::NONE => Self::None,
32 ffi::biometry::TOUCH_ID => Self::TouchId,
33 ffi::biometry::FACE_ID => Self::FaceId,
34 ffi::biometry::OPTIC_ID => Self::OpticId,
35 other => Self::Unknown(other),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[non_exhaustive]
43pub enum LACompanionType {
44 Watch,
45 Mac,
46 Vision,
47 Unknown(i32),
48}
49
50impl LACompanionType {
51 #[must_use]
52 pub const fn from_ffi(value: i32) -> Self {
53 match value {
54 ffi::companion::WATCH => Self::Watch,
55 ffi::companion::MAC => Self::Mac,
56 ffi::companion::VISION => Self::Vision,
57 other => Self::Unknown(other),
58 }
59 }
60
61 #[must_use]
62 pub const fn raw_value(self) -> i32 {
63 match self {
64 Self::Watch => ffi::companion::WATCH,
65 Self::Mac => ffi::companion::MAC,
66 Self::Vision => ffi::companion::VISION,
67 Self::Unknown(value) => value,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum LAAccessControlOperation {
76 CreateItem,
77 UseItem,
78 CreateKey,
79 UseKeySign,
80 UseKeyDecrypt,
81 UseKeyKeyExchange,
82}
83
84impl LAAccessControlOperation {
85 #[must_use]
86 pub const fn raw_value(self) -> i32 {
87 match self {
88 Self::CreateItem => ffi::la_context::ACCESS_CONTROL_OPERATION_CREATE_ITEM,
89 Self::UseItem => ffi::la_context::ACCESS_CONTROL_OPERATION_USE_ITEM,
90 Self::CreateKey => ffi::la_context::ACCESS_CONTROL_OPERATION_CREATE_KEY,
91 Self::UseKeySign => ffi::la_context::ACCESS_CONTROL_OPERATION_USE_KEY_SIGN,
92 Self::UseKeyDecrypt => ffi::la_context::ACCESS_CONTROL_OPERATION_USE_KEY_DECRYPT,
93 Self::UseKeyKeyExchange => {
94 ffi::la_context::ACCESS_CONTROL_OPERATION_USE_KEY_KEY_EXCHANGE
95 }
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct LADomainStateBiometry {
103 biometry_type: BiometryType,
104 state_hash: Option<Vec<u8>>,
105}
106
107impl LADomainStateBiometry {
108 #[must_use]
109 pub const fn biometry_type(&self) -> BiometryType {
110 self.biometry_type
111 }
112
113 #[must_use]
114 pub fn state_hash(&self) -> Option<&[u8]> {
115 self.state_hash.as_deref()
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct LADomainStateCompanion {
122 available_companion_types: Vec<LACompanionType>,
123 state_hash: Option<Vec<u8>>,
124 per_type_state_hashes: BTreeMap<LACompanionType, Vec<u8>>,
125}
126
127impl LADomainStateCompanion {
128 #[must_use]
129 pub fn available_companion_types(&self) -> &[LACompanionType] {
130 &self.available_companion_types
131 }
132
133 #[must_use]
134 pub fn state_hash(&self) -> Option<&[u8]> {
135 self.state_hash.as_deref()
136 }
137
138 #[must_use]
139 pub fn state_hash_for(&self, companion_type: LACompanionType) -> Option<&[u8]> {
140 self.per_type_state_hashes
141 .get(&companion_type)
142 .map(std::vec::Vec::as_slice)
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct LADomainState {
149 state_hash: Option<Vec<u8>>,
150 biometry: LADomainStateBiometry,
151 companion: Option<LADomainStateCompanion>,
152}
153
154impl LADomainState {
155 #[must_use]
156 pub fn state_hash(&self) -> Option<&[u8]> {
157 self.state_hash.as_deref()
158 }
159
160 #[must_use]
161 pub const fn biometry(&self) -> &LADomainStateBiometry {
162 &self.biometry
163 }
164
165 #[must_use]
166 pub const fn companion(&self) -> Option<&LADomainStateCompanion> {
167 self.companion.as_ref()
168 }
169}
170
171#[derive(Debug)]
173pub struct LAContext {
174 handle: OwnedHandle,
175}
176
177impl LAContext {
178 pub fn new() -> Result<Self> {
184 let raw = bridge_ptr(|out, error_out| unsafe {
185 ffi::la_context::la_context_new(out, error_out)
186 })?;
187 Ok(Self {
188 handle: OwnedHandle::new(raw, ffi::la_context::la_context_release),
189 })
190 }
191
192 pub fn invalidate(&self) -> Result<()> {
198 bridge_unit(|error_out| unsafe {
199 ffi::la_context::la_context_invalidate(self.handle.as_ptr(), error_out)
200 })
201 }
202
203 pub fn can_evaluate_policy(&self, policy: LAPolicy) -> Result<bool> {
209 let mut out_can_evaluate = 0_u8;
210 let mut framework_error_code = 0_i32;
211 let mut framework_error_message = ptr::null_mut();
212 let mut bridge_error = ptr::null_mut();
213
214 let status = unsafe {
215 ffi::la_context::la_context_can_evaluate_policy(
216 self.handle.as_ptr(),
217 policy.as_ffi(),
218 &mut out_can_evaluate,
219 &mut framework_error_code,
220 &mut framework_error_message,
221 &mut bridge_error,
222 )
223 };
224 if status != ffi::status::OK {
225 return Err(from_status(status, bridge_error));
226 }
227
228 framework_bool_result(
229 out_can_evaluate != 0,
230 framework_error_code,
231 framework_error_message,
232 )
233 }
234
235 pub fn evaluate_policy(&self, policy: LAPolicy, localized_reason: &str) -> Result<bool> {
241 if localized_reason.is_empty() {
242 return Err(LAError::InvalidArgument(
243 "localized reason must not be empty".to_owned(),
244 ));
245 }
246
247 let localized_reason = cstring(localized_reason)?;
248 bridge_bool(|out, error_out| unsafe {
249 ffi::la_context::la_context_evaluate_policy(
250 self.handle.as_ptr(),
251 policy.as_ffi(),
252 localized_reason.as_ptr(),
253 out,
254 error_out,
255 )
256 })
257 }
258
259 pub unsafe fn evaluate_access_control_raw(
269 &self,
270 access_control: *const c_void,
271 operation: LAAccessControlOperation,
272 localized_reason: &str,
273 ) -> Result<bool> {
274 if access_control.is_null() {
275 return Err(LAError::InvalidArgument(
276 "access control pointer must not be null".to_owned(),
277 ));
278 }
279 if localized_reason.is_empty() {
280 return Err(LAError::InvalidArgument(
281 "localized reason must not be empty".to_owned(),
282 ));
283 }
284
285 let localized_reason = cstring(localized_reason)?;
286 bridge_bool(|out, error_out| {
287 ffi::la_context::la_context_evaluate_access_control(
288 self.handle.as_ptr(),
289 access_control,
290 operation.raw_value(),
291 localized_reason.as_ptr(),
292 out,
293 error_out,
294 )
295 })
296 }
297
298 pub fn localized_fallback_title(&self) -> Result<Option<String>> {
304 bridge_opt_string(|out, error_out| unsafe {
305 ffi::la_context::la_context_get_localized_fallback_title(
306 self.handle.as_ptr(),
307 out,
308 error_out,
309 )
310 })
311 }
312
313 pub fn set_localized_fallback_title(&self, title: Option<&str>) -> Result<()> {
319 let title = title.map(cstring).transpose()?;
320 bridge_unit(|error_out| unsafe {
321 ffi::la_context::la_context_set_localized_fallback_title(
322 self.handle.as_ptr(),
323 title.as_ref().map_or(ptr::null(), |value| value.as_ptr()),
324 error_out,
325 )
326 })
327 }
328
329 pub fn localized_cancel_title(&self) -> Result<Option<String>> {
335 bridge_opt_string(|out, error_out| unsafe {
336 ffi::la_context::la_context_get_localized_cancel_title(
337 self.handle.as_ptr(),
338 out,
339 error_out,
340 )
341 })
342 }
343
344 pub fn set_localized_cancel_title(&self, title: Option<&str>) -> Result<()> {
350 let title = title.map(cstring).transpose()?;
351 bridge_unit(|error_out| unsafe {
352 ffi::la_context::la_context_set_localized_cancel_title(
353 self.handle.as_ptr(),
354 title.as_ref().map_or(ptr::null(), |value| value.as_ptr()),
355 error_out,
356 )
357 })
358 }
359
360 pub fn localized_reason(&self) -> Result<String> {
366 bridge_string(|out, error_out| unsafe {
367 ffi::la_context::la_context_get_localized_reason(self.handle.as_ptr(), out, error_out)
368 })
369 }
370
371 pub fn set_localized_reason(&self, localized_reason: &str) -> Result<()> {
377 let localized_reason = cstring(localized_reason)?;
378 bridge_unit(|error_out| unsafe {
379 ffi::la_context::la_context_set_localized_reason(
380 self.handle.as_ptr(),
381 localized_reason.as_ptr(),
382 error_out,
383 )
384 })
385 }
386
387 pub fn touch_id_authentication_allowable_reuse_duration(&self) -> Result<f64> {
393 bridge_f64(|out, error_out| unsafe {
394 ffi::la_context::la_context_get_touch_id_authentication_allowable_reuse_duration(
395 self.handle.as_ptr(),
396 out,
397 error_out,
398 )
399 })
400 }
401
402 pub fn allowable_reuse_duration(&self) -> Result<f64> {
408 self.touch_id_authentication_allowable_reuse_duration()
409 }
410
411 pub fn set_touch_id_authentication_allowable_reuse_duration(
417 &self,
418 duration: f64,
419 ) -> Result<()> {
420 if !duration.is_finite() || duration < 0.0 {
421 return Err(LAError::InvalidArgument(
422 "allowable reuse duration must be a finite, non-negative number".to_owned(),
423 ));
424 }
425
426 bridge_unit(|error_out| unsafe {
427 ffi::la_context::la_context_set_touch_id_authentication_allowable_reuse_duration(
428 self.handle.as_ptr(),
429 duration,
430 error_out,
431 )
432 })
433 }
434
435 pub fn set_allowable_reuse_duration(&self, duration: f64) -> Result<()> {
441 self.set_touch_id_authentication_allowable_reuse_duration(duration)
442 }
443
444 #[must_use]
446 pub fn touch_id_authentication_maximum_allowable_reuse_duration() -> f64 {
447 unsafe {
448 ffi::la_context::la_context_get_touch_id_authentication_maximum_allowable_reuse_duration(
449 )
450 }
451 }
452
453 pub fn interaction_not_allowed(&self) -> Result<bool> {
459 bridge_bool(|out, error_out| unsafe {
460 ffi::la_context::la_context_get_interaction_not_allowed(
461 self.handle.as_ptr(),
462 out,
463 error_out,
464 )
465 })
466 }
467
468 pub fn set_interaction_not_allowed(&self, value: bool) -> Result<()> {
474 bridge_unit(|error_out| unsafe {
475 ffi::la_context::la_context_set_interaction_not_allowed(
476 self.handle.as_ptr(),
477 u8::from(value),
478 error_out,
479 )
480 })
481 }
482
483 pub fn biometry_type(&self) -> Result<BiometryType> {
489 let raw = bridge_i32(|out, error_out| unsafe {
490 ffi::la_context::la_context_get_biometry_type(self.handle.as_ptr(), out, error_out)
491 })?;
492 Ok(BiometryType::from_ffi(raw))
493 }
494
495 pub fn evaluated_policy_domain_state(&self) -> Result<Option<Vec<u8>>> {
501 bridge_opt_bytes(|out, out_len, error_out| unsafe {
502 ffi::la_context::la_context_get_evaluated_policy_domain_state(
503 self.handle.as_ptr(),
504 out,
505 out_len,
506 error_out,
507 )
508 })
509 }
510
511 pub fn set_credential(&self, credential: &LACredential) -> Result<bool> {
517 let bytes = credential.bytes();
518 bridge_bool(|out, error_out| unsafe {
519 ffi::la_context::la_context_set_credential(
520 self.handle.as_ptr(),
521 bytes.as_ptr(),
522 bytes.len(),
523 credential.credential_type().as_ffi(),
524 1,
525 out,
526 error_out,
527 )
528 })
529 }
530
531 pub fn clear_credential(&self, credential_type: LACredentialType) -> Result<bool> {
537 bridge_bool(|out, error_out| unsafe {
538 ffi::la_context::la_context_set_credential(
539 self.handle.as_ptr(),
540 ptr::null(),
541 0,
542 credential_type.as_ffi(),
543 0,
544 out,
545 error_out,
546 )
547 })
548 }
549
550 pub fn is_credential_set(&self, credential_type: LACredentialType) -> Result<bool> {
556 bridge_bool(|out, error_out| unsafe {
557 ffi::la_context::la_context_is_credential_set(
558 self.handle.as_ptr(),
559 credential_type.as_ffi(),
560 out,
561 error_out,
562 )
563 })
564 }
565
566 pub fn domain_state(&self) -> Result<LADomainState> {
572 let state_hash = bridge_opt_bytes(|out, out_len, error_out| unsafe {
573 ffi::la_context::la_context_get_domain_state_hash(
574 self.handle.as_ptr(),
575 out,
576 out_len,
577 error_out,
578 )
579 })?;
580 let biometry_type = BiometryType::from_ffi(bridge_i32(|out, error_out| unsafe {
581 ffi::la_context::la_context_get_domain_state_biometry_type(
582 self.handle.as_ptr(),
583 out,
584 error_out,
585 )
586 })?);
587 let biometry_state_hash = bridge_opt_bytes(|out, out_len, error_out| unsafe {
588 ffi::la_context::la_context_get_domain_state_biometry_hash(
589 self.handle.as_ptr(),
590 out,
591 out_len,
592 error_out,
593 )
594 })?;
595 let companion_types_raw = bridge_i32_vec(|out, out_len, error_out| unsafe {
596 ffi::la_context::la_context_get_domain_state_companion_types(
597 self.handle.as_ptr(),
598 out,
599 out_len,
600 error_out,
601 )
602 })?;
603 let companion_types: Vec<LACompanionType> = companion_types_raw
604 .into_iter()
605 .map(LACompanionType::from_ffi)
606 .collect();
607 let companion_state_hash = bridge_opt_bytes(|out, out_len, error_out| unsafe {
608 ffi::la_context::la_context_get_domain_state_companion_hash(
609 self.handle.as_ptr(),
610 out,
611 out_len,
612 error_out,
613 )
614 })?;
615
616 let mut per_type_state_hashes = BTreeMap::new();
617 for companion_type in &companion_types {
618 if let Some(hash) = bridge_opt_bytes(|out, out_len, error_out| unsafe {
619 ffi::la_context::la_context_get_domain_state_companion_hash_for_type(
620 self.handle.as_ptr(),
621 companion_type.raw_value(),
622 out,
623 out_len,
624 error_out,
625 )
626 })? {
627 per_type_state_hashes.insert(*companion_type, hash);
628 }
629 }
630
631 Ok(LADomainState {
632 state_hash,
633 biometry: LADomainStateBiometry {
634 biometry_type,
635 state_hash: biometry_state_hash,
636 },
637 companion: Some(LADomainStateCompanion {
638 available_companion_types: companion_types,
639 state_hash: companion_state_hash,
640 per_type_state_hashes,
641 }),
642 })
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::{LACompanionType, LAContext, Result};
649 use crate::{LACredential, LACredentialType, LAPolicy};
650
651 #[test]
652 fn property_round_trip_without_prompt() -> Result<()> {
653 let context = LAContext::new()?;
654 context.set_interaction_not_allowed(true)?;
655 context.set_localized_fallback_title(Some("Use Password"))?;
656 context.set_localized_cancel_title(Some("Cancel"))?;
657 context.set_localized_reason("Test local authentication")?;
658 context.set_allowable_reuse_duration(30.0)?;
659 let credential = LACredential::application_password(b"secret".to_vec());
660
661 assert!(context.set_credential(&credential)?);
662 assert!(context.is_credential_set(LACredentialType::ApplicationPassword)?);
663 assert!(context.clear_credential(LACredentialType::ApplicationPassword)?);
664 assert!(!context.is_credential_set(LACredentialType::ApplicationPassword)?);
665 assert!(context.interaction_not_allowed()?);
666 assert_eq!(
667 context.localized_fallback_title()?.as_deref(),
668 Some("Use Password")
669 );
670 assert_eq!(context.localized_cancel_title()?.as_deref(), Some("Cancel"));
671 assert_eq!(context.localized_reason()?, "Test local authentication");
672 assert!((context.allowable_reuse_duration()? - 30.0).abs() < f64::EPSILON);
673 assert!(LAContext::touch_id_authentication_maximum_allowable_reuse_duration() >= 300.0);
674
675 let _ = context.can_evaluate_policy(LAPolicy::DeviceOwnerAuthenticationWithBiometrics);
676 let domain_state = context.domain_state()?;
677 let _ = domain_state.biometry().biometry_type();
678 if let Some(companion) = domain_state.companion() {
679 for companion_type in companion.available_companion_types() {
680 let _ = companion.state_hash_for(*companion_type);
681 }
682 let _ = companion.state_hash_for(LACompanionType::Watch);
683 }
684 Ok(())
685 }
686}