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