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}