Skip to main content

localauthentication/
la_context.rs

1//! `LAContext` and related `LocalAuthentication` value types.
2
3use 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/// Biometry kinds reported by `LAContext`.
17#[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/// Companion kinds reported by `LADomainState`.
41#[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/// Access-control operations supported by `LAContext::evaluate_access_control_raw`.
73#[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/// A snapshot of `LAContext.domainState.biometry`.
101#[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/// A snapshot of `LAContext.domainState.companion`.
120#[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/// A snapshot of `LAContext.domainState`.
147#[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/// Managed wrapper around Apple's `LAContext`.
172#[derive(Debug)]
173pub struct LAContext {
174    handle: OwnedHandle,
175}
176
177impl LAContext {
178    /// Create a new authentication context.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the Swift bridge fails to allocate the underlying `LAContext`.
183    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    /// Invalidate this context.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the Swift bridge rejects the request.
197    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    /// Check whether a policy can be evaluated without prompting the user.
204    ///
205    /// # Errors
206    ///
207    /// Returns a mapped framework error when the policy is unavailable, or a bridge error if the request itself fails.
208    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    /// Evaluate a policy with the supplied localized reason string.
236    ///
237    /// # Errors
238    ///
239    /// Returns a mapped framework or bridge error when evaluation fails.
240    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    /// Evaluate a `SecAccessControlRef` for the given operation.
260    ///
261    /// # Safety
262    ///
263    /// `access_control` must be a valid borrowed `SecAccessControlRef` for the duration of the call.
264    ///
265    /// # Errors
266    ///
267    /// Returns a mapped framework or bridge error when evaluation fails.
268    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    /// Read the localized fallback title.
299    ///
300    /// # Errors
301    ///
302    /// Returns an error if the Swift bridge rejects the request.
303    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    /// Update the localized fallback title. Pass `None` to restore the default title.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if the title contains an interior NUL byte or the Swift bridge rejects the request.
318    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    /// Read the localized cancel title.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if the Swift bridge rejects the request.
334    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    /// Update the localized cancel title. Pass `None` to restore the default title.
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if the title contains an interior NUL byte or the Swift bridge rejects the request.
349    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    /// Read the default localized reason used for authentication requests.
361    ///
362    /// # Errors
363    ///
364    /// Returns an error if the Swift bridge rejects the request.
365    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    /// Update the default localized reason used for authentication requests.
372    ///
373    /// # Errors
374    ///
375    /// Returns an error if the string contains an interior NUL byte or the Swift bridge rejects the request.
376    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    /// Read the allowable biometric reuse duration, in seconds.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if the Swift bridge rejects the request.
392    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    /// Backward-compatible alias for `touch_id_authentication_allowable_reuse_duration`.
403    ///
404    /// # Errors
405    ///
406    /// Propagates any error returned by `touch_id_authentication_allowable_reuse_duration`.
407    pub fn allowable_reuse_duration(&self) -> Result<f64> {
408        self.touch_id_authentication_allowable_reuse_duration()
409    }
410
411    /// Update the allowable biometric reuse duration, in seconds.
412    ///
413    /// # Errors
414    ///
415    /// Returns an error if the value is negative, non-finite, or the Swift bridge rejects the request.
416    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    /// Backward-compatible alias for `set_touch_id_authentication_allowable_reuse_duration`.
436    ///
437    /// # Errors
438    ///
439    /// Propagates any error returned by `set_touch_id_authentication_allowable_reuse_duration`.
440    pub fn set_allowable_reuse_duration(&self, duration: f64) -> Result<()> {
441        self.set_touch_id_authentication_allowable_reuse_duration(duration)
442    }
443
444    /// The framework-defined maximum reuse duration, in seconds.
445    #[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    /// Read whether interactive authentication UI is disabled.
454    ///
455    /// # Errors
456    ///
457    /// Returns an error if the Swift bridge rejects the request.
458    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    /// Enable or disable interactive authentication UI.
469    ///
470    /// # Errors
471    ///
472    /// Returns an error if the Swift bridge rejects the request.
473    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    /// Read the currently reported biometry type.
484    ///
485    /// # Errors
486    ///
487    /// Returns an error if the Swift bridge rejects the request.
488    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    /// Read the evaluated policy domain state bytes, if any are available.
496    ///
497    /// # Errors
498    ///
499    /// Returns an error if the Swift bridge rejects the request.
500    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    /// Set an application-provided credential for subsequent authentication operations.
512    ///
513    /// # Errors
514    ///
515    /// Returns an error if the Swift bridge rejects the request.
516    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    /// Remove any previously-supplied credential of the given type.
532    ///
533    /// # Errors
534    ///
535    /// Returns an error if the Swift bridge rejects the request.
536    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    /// Check whether a credential of the given type is currently stored on this context.
551    ///
552    /// # Errors
553    ///
554    /// Returns an error if the Swift bridge rejects the request.
555    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    /// Read the richer `domainState` snapshot available on macOS 15 and newer.
567    ///
568    /// # Errors
569    ///
570    /// Returns an error if the property is unavailable or the Swift bridge rejects the request.
571    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    /// Internal helper to get the raw pointer for FFI calls.
646    ///
647    /// Used by the async API module. This is intentionally non-public.
648    #[cfg(feature = "async")]
649    pub(crate) const fn as_ptr(&self) -> *mut c_void {
650        self.handle.as_ptr()
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::{LACompanionType, LAContext, Result};
657    use crate::{LACredential, LACredentialType, LAPolicy};
658
659    #[test]
660    fn property_round_trip_without_prompt() -> Result<()> {
661        let context = LAContext::new()?;
662        context.set_interaction_not_allowed(true)?;
663        context.set_localized_fallback_title(Some("Use Password"))?;
664        context.set_localized_cancel_title(Some("Cancel"))?;
665        context.set_localized_reason("Test local authentication")?;
666        context.set_allowable_reuse_duration(30.0)?;
667        let credential = LACredential::application_password(b"secret".to_vec());
668
669        assert!(context.set_credential(&credential)?);
670        assert!(context.is_credential_set(LACredentialType::ApplicationPassword)?);
671        assert!(context.clear_credential(LACredentialType::ApplicationPassword)?);
672        assert!(!context.is_credential_set(LACredentialType::ApplicationPassword)?);
673        assert!(context.interaction_not_allowed()?);
674        assert_eq!(
675            context.localized_fallback_title()?.as_deref(),
676            Some("Use Password")
677        );
678        assert_eq!(context.localized_cancel_title()?.as_deref(), Some("Cancel"));
679        assert_eq!(context.localized_reason()?, "Test local authentication");
680        assert!((context.allowable_reuse_duration()? - 30.0).abs() < f64::EPSILON);
681        assert!(LAContext::touch_id_authentication_maximum_allowable_reuse_duration() >= 300.0);
682
683        let _ = context.can_evaluate_policy(LAPolicy::DeviceOwnerAuthenticationWithBiometrics);
684        let domain_state = context.domain_state()?;
685        let _ = domain_state.biometry().biometry_type();
686        if let Some(companion) = domain_state.companion() {
687            for companion_type in companion.available_companion_types() {
688                let _ = companion.state_hash_for(*companion_type);
689            }
690            let _ = companion.state_hash_for(LACompanionType::Watch);
691        }
692        Ok(())
693    }
694}