Skip to main content

localauthentication/
la_environment.rs

1//! `LAEnvironment` observer, state, and mechanism wrappers.
2
3use core::ffi::c_void;
4use std::panic::{catch_unwind, AssertUnwindSafe};
5use std::ptr::NonNull;
6
7use crate::ffi;
8use crate::la_context::{BiometryType, LACompanionType};
9use crate::la_error::{LAError, Result};
10use crate::private::{
11    bridge_bool, bridge_bytes, bridge_i32, bridge_i64, bridge_opt_bytes, bridge_opt_ptr,
12    bridge_ptr, bridge_string, bridge_unit, OwnedHandle,
13};
14
15fn mechanism_is_usable(ptr: *mut c_void) -> Result<bool> {
16    bridge_bool(|out, error_out| unsafe {
17        ffi::la_environment::la_environment_mechanism_get_is_usable(ptr, out, error_out)
18    })
19}
20
21fn mechanism_localized_name(ptr: *mut c_void) -> Result<String> {
22    bridge_string(|out, error_out| unsafe {
23        ffi::la_environment::la_environment_mechanism_get_localized_name(ptr, out, error_out)
24    })
25}
26
27fn mechanism_icon_system_name(ptr: *mut c_void) -> Result<String> {
28    bridge_string(|out, error_out| unsafe {
29        ffi::la_environment::la_environment_mechanism_get_icon_system_name(ptr, out, error_out)
30    })
31}
32
33fn count_to_usize(count: i64, label: &str) -> Result<usize> {
34    usize::try_from(count)
35        .map_err(|_| LAError::BridgeFailed(format!("LocalAuthentication returned an invalid {label} count")))
36}
37
38/// Observer callbacks for `LAEnvironment` state changes.
39pub trait LAEnvironmentObserver: Send + Sync + 'static {
40    /// Invoked after `environment` has transitioned away from `old_state`.
41    fn state_did_change(&self, environment: &LAEnvironment, old_state: &LAEnvironmentState);
42}
43
44impl<F> LAEnvironmentObserver for F
45where
46    F: Fn(&LAEnvironment, &LAEnvironmentState) + Send + Sync + 'static,
47{
48    fn state_did_change(&self, environment: &LAEnvironment, old_state: &LAEnvironmentState) {
49        self(environment, old_state);
50    }
51}
52
53struct EnvironmentObserverContext {
54    observer: Box<dyn LAEnvironmentObserver>,
55}
56
57unsafe extern "C" fn environment_observer_trampoline(
58    context: *mut c_void,
59    environment_ptr: *mut c_void,
60    old_state_ptr: *mut c_void,
61) {
62    let Some(context) = NonNull::new(context.cast::<EnvironmentObserverContext>()) else {
63        return;
64    };
65    let Some(environment_raw) = NonNull::new(environment_ptr) else {
66        return;
67    };
68    let Some(old_state_raw) = NonNull::new(old_state_ptr) else {
69        return;
70    };
71
72    let context = unsafe { context.as_ref() };
73    let environment = LAEnvironment::from_raw(environment_raw);
74    let old_state = LAEnvironmentState::from_raw(old_state_raw);
75
76    let _ = catch_unwind(AssertUnwindSafe(|| {
77        context.observer.state_did_change(&environment, &old_state);
78    }));
79}
80
81unsafe extern "C" fn environment_observer_release(context: *mut c_void) {
82    if let Some(context) = NonNull::new(context.cast::<EnvironmentObserverContext>()) {
83        unsafe { drop(Box::from_raw(context.as_ptr())) };
84    }
85}
86
87/// Strong registration that keeps an `LAEnvironmentObserver` alive while `LAEnvironment` only holds it weakly.
88#[derive(Debug)]
89pub struct LAEnvironmentObserverRegistration {
90    handle: OwnedHandle,
91}
92
93impl LAEnvironmentObserverRegistration {
94    pub(crate) const fn as_ptr(&self) -> *mut c_void {
95        self.handle.as_ptr()
96    }
97}
98
99/// Managed wrapper around Apple's `LAEnvironment`.
100#[derive(Debug)]
101pub struct LAEnvironment {
102    handle: OwnedHandle,
103}
104
105impl LAEnvironment {
106    pub(crate) fn from_raw(raw: NonNull<c_void>) -> Self {
107        Self {
108            handle: OwnedHandle::new(raw, ffi::la_environment::la_environment_release),
109        }
110    }
111
112    #[allow(dead_code)]
113    pub(crate) const fn as_ptr(&self) -> *mut c_void {
114        self.handle.as_ptr()
115    }
116
117    /// The current user's authentication environment.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the macOS 15 environment APIs are unavailable or the Swift bridge rejects the request.
122    pub fn current_user() -> Result<Self> {
123        Ok(Self::from_raw(bridge_ptr(|out, error_out| unsafe {
124            ffi::la_environment::la_environment_current_user(out, error_out)
125        })?))
126    }
127
128    /// Snapshot the current environment state.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the macOS 15 environment APIs are unavailable or the Swift bridge rejects the request.
133    pub fn state(&self) -> Result<LAEnvironmentState> {
134        Ok(LAEnvironmentState::from_raw(bridge_ptr(
135            |out, error_out| unsafe {
136                ffi::la_environment::la_environment_get_state(self.handle.as_ptr(), out, error_out)
137            },
138        )?))
139    }
140
141    /// Register an observer for environment state changes.
142    ///
143    /// Keep the returned registration alive for as long as the observer should remain registered.
144    /// Dropping it releases the weakly-held observer object.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if the macOS 15 environment APIs are unavailable or the Swift bridge rejects the request.
149    pub fn add_observer<O>(&self, observer: O) -> Result<LAEnvironmentObserverRegistration>
150    where
151        O: LAEnvironmentObserver,
152    {
153        let context = Box::new(EnvironmentObserverContext {
154            observer: Box::new(observer),
155        });
156        let context_ptr = Box::into_raw(context).cast::<c_void>();
157
158        let observer_raw = match bridge_ptr(|out, error_out| unsafe {
159            ffi::la_environment::la_environment_observer_new(
160                Some(environment_observer_trampoline),
161                Some(environment_observer_release),
162                context_ptr,
163                out,
164                error_out,
165            )
166        }) {
167            Ok(raw) => raw,
168            Err(error) => {
169                unsafe { environment_observer_release(context_ptr) };
170                return Err(error);
171            }
172        };
173
174        let registration = LAEnvironmentObserverRegistration {
175            handle: OwnedHandle::new(
176                observer_raw,
177                ffi::la_environment::la_environment_observer_release,
178            ),
179        };
180
181        if let Err(error) = bridge_unit(|error_out| unsafe {
182            ffi::la_environment::la_environment_add_observer(
183                self.handle.as_ptr(),
184                registration.handle.as_ptr(),
185                error_out,
186            )
187        }) {
188            drop(registration);
189            return Err(error);
190        }
191
192        Ok(registration)
193    }
194
195    /// Remove a previously registered observer.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the macOS 15 environment APIs are unavailable or the Swift bridge rejects the request.
200    pub fn remove_observer(&self, observer: &LAEnvironmentObserverRegistration) -> Result<()> {
201        bridge_unit(|error_out| unsafe {
202            ffi::la_environment::la_environment_remove_observer(
203                self.handle.as_ptr(),
204                observer.as_ptr(),
205                error_out,
206            )
207        })
208    }
209}
210
211/// Snapshot wrapper around Apple's `LAEnvironmentState`.
212#[derive(Debug)]
213pub struct LAEnvironmentState {
214    handle: OwnedHandle,
215}
216
217impl LAEnvironmentState {
218    pub(crate) fn from_raw(raw: NonNull<c_void>) -> Self {
219        Self {
220            handle: OwnedHandle::new(raw, ffi::la_environment::la_environment_state_release),
221        }
222    }
223
224    #[allow(dead_code)]
225    pub(crate) const fn as_ptr(&self) -> *mut c_void {
226        self.handle.as_ptr()
227    }
228
229    /// Information about the device's biometric mechanism, if supported.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the Swift bridge rejects the request.
234    pub fn biometry(&self) -> Result<Option<LAEnvironmentMechanismBiometry>> {
235        let raw = bridge_opt_ptr(|out, error_out| unsafe {
236            ffi::la_environment::la_environment_state_get_biometry(self.handle.as_ptr(), out, error_out)
237        })?;
238        Ok(raw.map(LAEnvironmentMechanismBiometry::from_raw))
239    }
240
241    /// Information about the local user password or passcode mechanism, if supported.
242    ///
243    /// # Errors
244    ///
245    /// Returns an error if the Swift bridge rejects the request.
246    pub fn user_password(&self) -> Result<Option<LAEnvironmentMechanismUserPassword>> {
247        let raw = bridge_opt_ptr(|out, error_out| unsafe {
248            ffi::la_environment::la_environment_state_get_user_password(
249                self.handle.as_ptr(),
250                out,
251                error_out,
252            )
253        })?;
254        Ok(raw.map(LAEnvironmentMechanismUserPassword::from_raw))
255    }
256
257    /// Companion authentication mechanisms currently paired with this device.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if the Swift bridge rejects the request.
262    pub fn companions(&self) -> Result<Vec<LAEnvironmentMechanismCompanion>> {
263        let count = count_to_usize(
264            bridge_i64(|out, error_out| unsafe {
265                ffi::la_environment::la_environment_state_get_companion_count(
266                    self.handle.as_ptr(),
267                    out,
268                    error_out,
269                )
270            })?,
271            "companion",
272        )?;
273
274        let mut mechanisms = Vec::with_capacity(count);
275        for index in 0..count {
276            let index = i64::try_from(index).map_err(|_| {
277                LAError::BridgeFailed(
278                    "LocalAuthentication returned more companion mechanisms than this platform can index"
279                        .to_owned(),
280                )
281            })?;
282            let raw = bridge_ptr(|out, error_out| unsafe {
283                ffi::la_environment::la_environment_state_get_companion_at(
284                    self.handle.as_ptr(),
285                    index,
286                    out,
287                    error_out,
288                )
289            })?;
290            mechanisms.push(LAEnvironmentMechanismCompanion::from_raw(raw));
291        }
292        Ok(mechanisms)
293    }
294
295    /// Information about every currently known authentication mechanism.
296    ///
297    /// # Errors
298    ///
299    /// Returns an error if the Swift bridge rejects the request.
300    pub fn all_mechanisms(&self) -> Result<Vec<LAEnvironmentMechanism>> {
301        let count = count_to_usize(
302            bridge_i64(|out, error_out| unsafe {
303                ffi::la_environment::la_environment_state_get_all_mechanism_count(
304                    self.handle.as_ptr(),
305                    out,
306                    error_out,
307                )
308            })?,
309            "mechanism",
310        )?;
311
312        let mut mechanisms = Vec::with_capacity(count);
313        for index in 0..count {
314            let index = i64::try_from(index).map_err(|_| {
315                LAError::BridgeFailed(
316                    "LocalAuthentication returned more mechanisms than this platform can index"
317                        .to_owned(),
318                )
319            })?;
320            let raw = bridge_ptr(|out, error_out| unsafe {
321                ffi::la_environment::la_environment_state_get_all_mechanism_at(
322                    self.handle.as_ptr(),
323                    index,
324                    out,
325                    error_out,
326                )
327            })?;
328            mechanisms.push(LAEnvironmentMechanism::from_raw(raw));
329        }
330        Ok(mechanisms)
331    }
332}
333
334/// Common properties shared by every `LAEnvironment` authentication mechanism.
335#[derive(Debug)]
336pub struct LAEnvironmentMechanism {
337    handle: OwnedHandle,
338}
339
340impl LAEnvironmentMechanism {
341    pub(crate) fn from_raw(raw: NonNull<c_void>) -> Self {
342        Self {
343            handle: OwnedHandle::new(raw, ffi::la_environment::la_environment_mechanism_release),
344        }
345    }
346
347    #[allow(dead_code)]
348    pub(crate) const fn as_ptr(&self) -> *mut c_void {
349        self.handle.as_ptr()
350    }
351
352    /// Whether the mechanism is currently usable for authentication.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if the Swift bridge rejects the request.
357    pub fn is_usable(&self) -> Result<bool> {
358        mechanism_is_usable(self.handle.as_ptr())
359    }
360
361    /// Localized display name such as `Touch ID` or `Password`.
362    ///
363    /// # Errors
364    ///
365    /// Returns an error if the Swift bridge rejects the request.
366    pub fn localized_name(&self) -> Result<String> {
367        mechanism_localized_name(self.handle.as_ptr())
368    }
369
370    /// SF Symbol name representing this mechanism.
371    ///
372    /// # Errors
373    ///
374    /// Returns an error if the Swift bridge rejects the request.
375    pub fn icon_system_name(&self) -> Result<String> {
376        mechanism_icon_system_name(self.handle.as_ptr())
377    }
378}
379
380/// Biometric `LAEnvironment` mechanism details.
381#[derive(Debug)]
382pub struct LAEnvironmentMechanismBiometry {
383    handle: OwnedHandle,
384}
385
386impl LAEnvironmentMechanismBiometry {
387    pub(crate) fn from_raw(raw: NonNull<c_void>) -> Self {
388        Self {
389            handle: OwnedHandle::new(raw, ffi::la_environment::la_environment_mechanism_release),
390        }
391    }
392
393    #[allow(dead_code)]
394    pub(crate) const fn as_ptr(&self) -> *mut c_void {
395        self.handle.as_ptr()
396    }
397
398    /// Whether the mechanism is currently usable for authentication.
399    ///
400    /// # Errors
401    ///
402    /// Returns an error if the Swift bridge rejects the request.
403    pub fn is_usable(&self) -> Result<bool> {
404        mechanism_is_usable(self.handle.as_ptr())
405    }
406
407    /// Localized display name such as `Touch ID` or `Face ID`.
408    ///
409    /// # Errors
410    ///
411    /// Returns an error if the Swift bridge rejects the request.
412    pub fn localized_name(&self) -> Result<String> {
413        mechanism_localized_name(self.handle.as_ptr())
414    }
415
416    /// SF Symbol name representing this mechanism.
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if the Swift bridge rejects the request.
421    pub fn icon_system_name(&self) -> Result<String> {
422        mechanism_icon_system_name(self.handle.as_ptr())
423    }
424
425    /// Hardware biometry type supported by the device.
426    ///
427    /// # Errors
428    ///
429    /// Returns an error if the Swift bridge rejects the request.
430    pub fn biometry_type(&self) -> Result<BiometryType> {
431        Ok(BiometryType::from_ffi(bridge_i32(|out, error_out| unsafe {
432            ffi::la_environment::la_environment_mechanism_biometry_get_biometry_type(
433                self.handle.as_ptr(),
434                out,
435                error_out,
436            )
437        })?))
438    }
439
440    /// Whether the user has enrolled this biometric mechanism.
441    ///
442    /// # Errors
443    ///
444    /// Returns an error if the Swift bridge rejects the request.
445    pub fn is_enrolled(&self) -> Result<bool> {
446        bridge_bool(|out, error_out| unsafe {
447            ffi::la_environment::la_environment_mechanism_biometry_get_is_enrolled(
448                self.handle.as_ptr(),
449                out,
450                error_out,
451            )
452        })
453    }
454
455    /// Whether the biometric mechanism is locked out.
456    ///
457    /// # Errors
458    ///
459    /// Returns an error if the Swift bridge rejects the request.
460    pub fn is_locked_out(&self) -> Result<bool> {
461        bridge_bool(|out, error_out| unsafe {
462            ffi::la_environment::la_environment_mechanism_biometry_get_is_locked_out(
463                self.handle.as_ptr(),
464                out,
465                error_out,
466            )
467        })
468    }
469
470    /// Application-specific biometric enrollment hash.
471    ///
472    /// # Errors
473    ///
474    /// Returns an error if the Swift bridge rejects the request.
475    pub fn state_hash(&self) -> Result<Vec<u8>> {
476        bridge_bytes(|out, out_len, error_out| unsafe {
477            ffi::la_environment::la_environment_mechanism_biometry_get_state_hash(
478                self.handle.as_ptr(),
479                out,
480                out_len,
481                error_out,
482            )
483        })
484    }
485
486    /// Whether the built-in biometric sensor is inaccessible.
487    ///
488    /// # Errors
489    ///
490    /// Returns an error if the Swift bridge rejects the request.
491    pub fn built_in_sensor_inaccessible(&self) -> Result<bool> {
492        bridge_bool(|out, error_out| unsafe {
493            ffi::la_environment::la_environment_mechanism_biometry_get_built_in_sensor_inaccessible(
494                self.handle.as_ptr(),
495                out,
496                error_out,
497            )
498        })
499    }
500}
501
502/// Companion-device `LAEnvironment` mechanism details.
503#[derive(Debug)]
504pub struct LAEnvironmentMechanismCompanion {
505    handle: OwnedHandle,
506}
507
508impl LAEnvironmentMechanismCompanion {
509    pub(crate) fn from_raw(raw: NonNull<c_void>) -> Self {
510        Self {
511            handle: OwnedHandle::new(raw, ffi::la_environment::la_environment_mechanism_release),
512        }
513    }
514
515    #[allow(dead_code)]
516    pub(crate) const fn as_ptr(&self) -> *mut c_void {
517        self.handle.as_ptr()
518    }
519
520    /// Whether the mechanism is currently usable for authentication.
521    ///
522    /// # Errors
523    ///
524    /// Returns an error if the Swift bridge rejects the request.
525    pub fn is_usable(&self) -> Result<bool> {
526        mechanism_is_usable(self.handle.as_ptr())
527    }
528
529    /// Localized display name such as `Apple Watch`.
530    ///
531    /// # Errors
532    ///
533    /// Returns an error if the Swift bridge rejects the request.
534    pub fn localized_name(&self) -> Result<String> {
535        mechanism_localized_name(self.handle.as_ptr())
536    }
537
538    /// SF Symbol name representing this mechanism.
539    ///
540    /// # Errors
541    ///
542    /// Returns an error if the Swift bridge rejects the request.
543    pub fn icon_system_name(&self) -> Result<String> {
544        mechanism_icon_system_name(self.handle.as_ptr())
545    }
546
547    /// Companion-device type.
548    ///
549    /// # Errors
550    ///
551    /// Returns an error if the Swift bridge rejects the request.
552    pub fn companion_type(&self) -> Result<LACompanionType> {
553        Ok(LACompanionType::from_ffi(bridge_i32(|out, error_out| unsafe {
554            ffi::la_environment::la_environment_mechanism_companion_get_type(
555                self.handle.as_ptr(),
556                out,
557                error_out,
558            )
559        })?))
560    }
561
562    /// Pairing hash for the current companion type, if one exists.
563    ///
564    /// # Errors
565    ///
566    /// Returns an error if the Swift bridge rejects the request.
567    pub fn state_hash(&self) -> Result<Option<Vec<u8>>> {
568        bridge_opt_bytes(|out, out_len, error_out| unsafe {
569            ffi::la_environment::la_environment_mechanism_companion_get_state_hash(
570                self.handle.as_ptr(),
571                out,
572                out_len,
573                error_out,
574            )
575        })
576    }
577}
578
579/// Password or passcode `LAEnvironment` mechanism details.
580#[derive(Debug)]
581pub struct LAEnvironmentMechanismUserPassword {
582    handle: OwnedHandle,
583}
584
585impl LAEnvironmentMechanismUserPassword {
586    pub(crate) fn from_raw(raw: NonNull<c_void>) -> Self {
587        Self {
588            handle: OwnedHandle::new(raw, ffi::la_environment::la_environment_mechanism_release),
589        }
590    }
591
592    #[allow(dead_code)]
593    pub(crate) const fn as_ptr(&self) -> *mut c_void {
594        self.handle.as_ptr()
595    }
596
597    /// Whether the mechanism is currently usable for authentication.
598    ///
599    /// # Errors
600    ///
601    /// Returns an error if the Swift bridge rejects the request.
602    pub fn is_usable(&self) -> Result<bool> {
603        mechanism_is_usable(self.handle.as_ptr())
604    }
605
606    /// Localized display name such as `Password`.
607    ///
608    /// # Errors
609    ///
610    /// Returns an error if the Swift bridge rejects the request.
611    pub fn localized_name(&self) -> Result<String> {
612        mechanism_localized_name(self.handle.as_ptr())
613    }
614
615    /// SF Symbol name representing this mechanism.
616    ///
617    /// # Errors
618    ///
619    /// Returns an error if the Swift bridge rejects the request.
620    pub fn icon_system_name(&self) -> Result<String> {
621        mechanism_icon_system_name(self.handle.as_ptr())
622    }
623
624    /// Whether the local user password or passcode is set.
625    ///
626    /// # Errors
627    ///
628    /// Returns an error if the Swift bridge rejects the request.
629    pub fn is_set(&self) -> Result<bool> {
630        bridge_bool(|out, error_out| unsafe {
631            ffi::la_environment::la_environment_mechanism_user_password_get_is_set(
632                self.handle.as_ptr(),
633                out,
634                error_out,
635            )
636        })
637    }
638}