Skip to main content

robius_authentication/
lib.rs

1//! Abstractions for multi-platform native authentication.
2//!
3//! This crate supports:
4//! - Apple: TouchID, FaceID, and regular username/password on macOS and iOS.
5//! - Android: See below for additional steps.
6//!   - Requires the `USE_BIOMETRIC` permission in your app's manifest.
7//!   - Requires API level 28 (Android 9) for biometrics, or API level 29
8//!     (Android 10) if you use the password / device-credential fallback.
9//! - Windows: Windows Hello (face recognition, fingerprint, PIN) via WinRT, plus
10//!   a Win32 credential-UI fallback for username/password.
11//! - Linux: [`polkit`]-based authentication using the desktop environment's
12//!   prompt.
13//!   - **Note: Linux support is currently incomplete.**
14//!
15//! # Example
16//!
17//! ```no_run
18//! use robius_authentication::{
19//!     AndroidText, BiometricStrength, Context, Policy, PolicyBuilder, Text, WindowsText,
20//! };
21//!
22//! let policy: Policy = PolicyBuilder::new()
23//!     .biometrics(Some(BiometricStrength::Strong))
24//!     .password(true)
25//!     .companion(true)
26//!     // Required on Linux (polkit action IDs); a no-op on other platforms.
27//!     .action_ids(["org.robius.authentication"])
28//!     .build()
29//!     .unwrap();
30//!
31//! let text = Text {
32//!     android: AndroidText {
33//!         title: "Title",
34//!         subtitle: None,
35//!         description: None,
36//!     },
37//!     apple: "authenticate",
38//!     windows: WindowsText::new_truncated("Title", "Description"),
39//! };
40//!
41//! let callback = |auth_result| {
42//!     match auth_result {
43//!         Ok(_)  => println!("Authentication success!"),
44//!         Err(_) => eprintln!("Authentication failed!"),
45//!     }
46//! };
47//!
48//! Context::new(())
49//!     .authenticate(text, &policy, callback)
50//!     .expect("failed to display the authentication prompt");
51//! ```
52//!
53//! The `Text` struct can also be constructed at compile-time to avoid run-time unwraps:
54//! ```
55//! use robius_authentication::{AndroidText, Text, WindowsText};
56//!
57//! const TEXT: Text = Text {
58//!     android: AndroidText {
59//!         title: "Title",
60//!         subtitle: None,
61//!         description: None,
62//!     },
63//!     apple: "authenticate",
64//!     windows: match WindowsText::new("Title", "Description") {
65//!         Some(text) => text,
66//!         None => panic!("Windows text too long"),
67//!     },
68//! };
69//! ```
70//!
71//! For more details about the prompt text see the [`Text`] struct
72//! which allows you to customize the prompt for each platform.
73//!
74//! ## Usage on Android
75//!
76//! For authentication to work, the following must be added to your app's
77//! `AndroidManifest.xml`:
78//! ```xml
79//! <uses-permission android:name="android.permission.USE_BIOMETRIC" />
80//! ```
81//!
82//! ### Minimum API level
83//!
84//! This crate uses `android.hardware.biometrics.BiometricPrompt`, so the minimum
85//! supported API level is 28 (Android 9). The password / device-credential
86//! fallback needs API level 29 (Android 10); requesting a credential-only policy
87//! on a lower level returns [`Error::Unavailable`]. Set `minSdk` accordingly in
88//! your app.
89//!
90//! [`polkit`]: https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html
91
92mod error;
93mod sys;
94mod text;
95
96pub use crate::{
97    error::{Error, Result},
98    text::{AndroidText, Text, WindowsText},
99};
100
101/// A "raw" context that can be used to create a [`Context`].
102///
103/// Currently, all platforms define this as the void type `()`.
104pub type RawContext = sys::RawContext;
105
106/// Holds platform-specific contextual state required to display an authentication prompt.
107#[derive(Debug)]
108pub struct Context {
109    inner: sys::Context,
110}
111
112impl Context {
113    /// Creates a new context from the given "raw" context.
114    #[inline]
115    pub fn new(raw: RawContext) -> Self {
116        Self {
117            inner: sys::Context::new(raw),
118        }
119    }
120
121    // Async authentication functions are currently not implemented. 
122    //
123    // /// Authenticates using the provided policy and message.
124    // ///
125    // /// Returns whether the authentication was successful.
126    // #[inline]
127    // #[cfg(feature = "async")]
128    // pub async fn authenticate_async(
129    //     &self,
130    //     message: Text<'_, '_, '_, '_, '_, '_>,
131    //     policy: &Policy,
132    // ) -> Result<()> {
133    //     self.inner.authenticate(message, &policy.inner).await
134    // }
135
136    /// Displays an authentication prompt using the provided policy and message.
137    ///
138    /// Note that the returned `Result` does not indicate whether
139    /// authentication was successful. This function returns `Ok(())`
140    /// to indicate that the authentication request was successfully initiated
141    /// (i.e., the prompt was or will be shown), not that the user successfully
142    /// authenticated. On some platforms (Android, Linux, Windows) the prompt is
143    /// shown asynchronously after this function has already returned.
144    ///
145    /// For that purpose, the given `callback` will be called
146    /// with a Result indicating whether authentication succeeded.
147    /// If this function returns an error, the callback may never be invoked;
148    /// if it returns `Ok(())`, the callback will eventually be invoked
149    /// with the result of the authentication attempt.
150    ///
151    /// On Linux: `message` is unused because polkit looks up the prompt text from the
152    /// action definition in the installed `.policy` file (its `<message>`/`<description>`).
153    /// The client only supplies an action ID to `CheckAuthorization`, and there is no
154    /// stable, cross-agent way to override that UI text at runtime.
155    /// If you need multiple prompt strings, define multiple actions in the policy file
156    /// and select the desired one via `PolicyBuilder::action_ids` and
157    /// `Policy::set_action_id` before each authentication.
158    ///
159    /// Thus, authentication failed if this function returns an error
160    /// **OR** if the `callback` is invoked with `Err(_)`.
161    #[inline]
162    pub fn authenticate<F>(
163        &self,
164        message: Text,
165        policy: &Policy,
166        callback: F,
167    ) -> Result<()>
168    where
169        F: Fn(Result<()>) + Send + 'static,
170    {
171        self.inner.authenticate(message, &policy.inner, callback)
172    }
173}
174
175/// A biometric strength class.
176///
177/// This only has an effect on Android. On other targets, any biometric strength
178/// setting will enable all biometric authentication devices. See the [Android
179/// documentation][android-docs] for more details.
180///
181/// [android-docs]: https://source.android.com/docs/security/features/biometric
182#[derive(Debug)]
183pub enum BiometricStrength {
184    Strong,
185    Weak,
186}
187
188/// A builder for conveniently defining a policy.
189///
190/// It is **highly recommended** to use the [`Self::new()`] (default) value
191/// to create a policy with all options enabled, because each platform acts differently
192/// when being requested to enable/disable various authentication methods.
193/// Enabling all options is the safest way to ensure that the authentication prompt
194/// will be displayed correctly on all platforms.
195///
196/// On Linux, at least one polkit action ID *MUST* be explicitly provided.
197/// If not set (or empty), `build()` will return None.
198#[derive(Debug)]
199pub struct PolicyBuilder {
200    inner: sys::PolicyBuilder,
201}
202
203impl Default for PolicyBuilder {
204    #[inline]
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210impl PolicyBuilder {
211    /// Returns a new policy with sane defaults.
212    #[inline]
213    pub const fn new() -> Self {
214        Self {
215            inner: sys::PolicyBuilder::new(),
216        }
217    }
218
219    /// Sets the list of action identifiers that require authorization.
220    ///
221    /// On Linux systems, these map to polkit `action_id`s, which represent
222    /// specific privileged operations (such as modifying system settings or
223    /// powering off the device). The authentication backend uses the current
224    /// action ID to determine whether the action is permitted and whether
225    /// user authentication is required.
226    ///
227    /// The first entry is used as the default action ID. To switch between
228    /// multiple action IDs at runtime on Linux, build a policy once and call
229    /// [`Policy::set_action_id`] before each authentication. The setter
230    /// validates against this list.
231    ///
232    /// This only has an effect on linux.
233    #[inline]
234    #[must_use]
235    pub fn action_ids<I, S>(self, ids: I) -> Self
236    where
237        I: IntoIterator<Item = S>,
238        S: Into<String>,
239    {
240        let ids = ids.into_iter().map(Into::into).collect::<Vec<String>>();
241        Self {
242            inner: self.inner.action_ids(ids),
243        }
244    }
245
246    /// Configures biometric authentication with the given strength.
247    ///
248    /// The strength only has an effect on Android, see [`BiometricStrength`]
249    /// for more details.
250    #[inline]
251    #[must_use]
252    pub fn biometrics(self, strength: Option<BiometricStrength>) -> Self {
253        Self {
254            inner: self.inner.biometrics(strength),
255        }
256    }
257
258    /// Sets whether the policy supports passwords.
259    #[inline]
260    #[must_use]
261    pub fn password(self, password: bool) -> Self {
262        Self {
263            inner: self.inner.password(password),
264        }
265    }
266
267    /// Sets whether the policy supports authentication via a proximity companion device, e.g., Apple Watch.
268    ///
269    /// This only has an effect on iOS and macOS.
270    #[inline]
271    #[must_use]
272    pub fn companion(self, companion: bool) -> Self {
273        Self {
274            inner: self.inner.companion(companion),
275        }
276    }
277
278    /// Sets whether the policy requires the companion device (Apple Watch) to be on the user's wrist.
279    ///
280    /// This only has an effect on Apple watchOS.
281    #[inline]
282    #[must_use]
283    pub fn wrist_detection(self, wrist_detection: bool) -> Self {
284        Self {
285            inner: self.inner.wrist_detection(wrist_detection),
286        }
287    }
288
289    /// Constructs the policy.
290    ///
291    /// Returns `None` if the specified configuration is not valid for the
292    /// current target.
293    #[inline]
294    #[must_use]
295    pub fn build(self) -> Option<Policy> {
296        Some(Policy {
297            inner: self.inner.build()?,
298        })
299    }
300}
301
302/// An authentication policy.
303#[derive(Debug)]
304pub struct Policy {
305    inner: sys::Policy,
306}
307
308impl Policy {
309    /// Sets the polkit action ID used on Linux.
310    ///
311    /// Returns [`Error::InvalidActionId`] if the ID is not in the allowed list
312    /// provided via [`PolicyBuilder::action_ids`].
313    ///
314    /// This is a no-op on non-Linux platforms.
315    #[inline]
316    pub fn set_action_id(&mut self, id: impl Into<String>) -> Result<()> {
317        self.inner.set_action_id(id.into())
318    }
319}