robius-authentication 0.3.0

Rust abstractions for multi-platform native authentication: biometrics, fingerprint, password, screen lock, TouchID, FaceID, Windows Hello, etc.
Documentation
//! Abstractions for multi-platform native authentication.
//!
//! This crate supports:
//! - Apple: TouchID, FaceID, and regular username/password on macOS and iOS.
//! - Android: See below for additional steps.
//!   - Requires the `USE_BIOMETRIC` permission in your app's manifest.
//!   - Requires API level 28 (Android 9) for biometrics, or API level 29
//!     (Android 10) if you use the password / device-credential fallback.
//! - Windows: Windows Hello (face recognition, fingerprint, PIN) via WinRT, plus
//!   a Win32 credential-UI fallback for username/password.
//! - Linux: [`polkit`]-based authentication using the desktop environment's
//!   prompt.
//!   - **Note: Linux support is currently incomplete.**
//!
//! # Example
//!
//! ```no_run
//! use robius_authentication::{
//!     AndroidText, BiometricStrength, Context, Policy, PolicyBuilder, Text, WindowsText,
//! };
//!
//! let policy: Policy = PolicyBuilder::new()
//!     .biometrics(Some(BiometricStrength::Strong))
//!     .password(true)
//!     .companion(true)
//!     // Required on Linux (polkit action IDs); a no-op on other platforms.
//!     .action_ids(["org.robius.authentication"])
//!     .build()
//!     .unwrap();
//!
//! let text = Text {
//!     android: AndroidText {
//!         title: "Title",
//!         subtitle: None,
//!         description: None,
//!     },
//!     apple: "authenticate",
//!     windows: WindowsText::new_truncated("Title", "Description"),
//! };
//!
//! let callback = |auth_result| {
//!     match auth_result {
//!         Ok(_)  => println!("Authentication success!"),
//!         Err(_) => eprintln!("Authentication failed!"),
//!     }
//! };
//!
//! Context::new(())
//!     .authenticate(text, &policy, callback)
//!     .expect("failed to display the authentication prompt");
//! ```
//!
//! The `Text` struct can also be constructed at compile-time to avoid run-time unwraps:
//! ```
//! use robius_authentication::{AndroidText, Text, WindowsText};
//!
//! const TEXT: Text = Text {
//!     android: AndroidText {
//!         title: "Title",
//!         subtitle: None,
//!         description: None,
//!     },
//!     apple: "authenticate",
//!     windows: match WindowsText::new("Title", "Description") {
//!         Some(text) => text,
//!         None => panic!("Windows text too long"),
//!     },
//! };
//! ```
//!
//! For more details about the prompt text see the [`Text`] struct
//! which allows you to customize the prompt for each platform.
//!
//! ## Usage on Android
//!
//! For authentication to work, the following must be added to your app's
//! `AndroidManifest.xml`:
//! ```xml
//! <uses-permission android:name="android.permission.USE_BIOMETRIC" />
//! ```
//!
//! ### Minimum API level
//!
//! This crate uses `android.hardware.biometrics.BiometricPrompt`, so the minimum
//! supported API level is 28 (Android 9). The password / device-credential
//! fallback needs API level 29 (Android 10); requesting a credential-only policy
//! on a lower level returns [`Error::Unavailable`]. Set `minSdk` accordingly in
//! your app.
//!
//! [`polkit`]: https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html

mod error;
mod sys;
mod text;

pub use crate::{
    error::{Error, Result},
    text::{AndroidText, Text, WindowsText},
};

/// A "raw" context that can be used to create a [`Context`].
///
/// Currently, all platforms define this as the void type `()`.
pub type RawContext = sys::RawContext;

/// Holds platform-specific contextual state required to display an authentication prompt.
#[derive(Debug)]
pub struct Context {
    inner: sys::Context,
}

impl Context {
    /// Creates a new context from the given "raw" context.
    #[inline]
    pub fn new(raw: RawContext) -> Self {
        Self {
            inner: sys::Context::new(raw),
        }
    }

    // Async authentication functions are currently not implemented. 
    //
    // /// Authenticates using the provided policy and message.
    // ///
    // /// Returns whether the authentication was successful.
    // #[inline]
    // #[cfg(feature = "async")]
    // pub async fn authenticate_async(
    //     &self,
    //     message: Text<'_, '_, '_, '_, '_, '_>,
    //     policy: &Policy,
    // ) -> Result<()> {
    //     self.inner.authenticate(message, &policy.inner).await
    // }

    /// Displays an authentication prompt using the provided policy and message.
    ///
    /// Note that the returned `Result` does not indicate whether
    /// authentication was successful. This function returns `Ok(())`
    /// to indicate that the authentication request was successfully initiated
    /// (i.e., the prompt was or will be shown), not that the user successfully
    /// authenticated. On some platforms (Android, Linux, Windows) the prompt is
    /// shown asynchronously after this function has already returned.
    ///
    /// For that purpose, the given `callback` will be called
    /// with a Result indicating whether authentication succeeded.
    /// If this function returns an error, the callback may never be invoked;
    /// if it returns `Ok(())`, the callback will eventually be invoked
    /// with the result of the authentication attempt.
    ///
    /// On Linux: `message` is unused because polkit looks up the prompt text from the
    /// action definition in the installed `.policy` file (its `<message>`/`<description>`).
    /// The client only supplies an action ID to `CheckAuthorization`, and there is no
    /// stable, cross-agent way to override that UI text at runtime.
    /// If you need multiple prompt strings, define multiple actions in the policy file
    /// and select the desired one via `PolicyBuilder::action_ids` and
    /// `Policy::set_action_id` before each authentication.
    ///
    /// Thus, authentication failed if this function returns an error
    /// **OR** if the `callback` is invoked with `Err(_)`.
    #[inline]
    pub fn authenticate<F>(
        &self,
        message: Text,
        policy: &Policy,
        callback: F,
    ) -> Result<()>
    where
        F: Fn(Result<()>) + Send + 'static,
    {
        self.inner.authenticate(message, &policy.inner, callback)
    }
}

/// A biometric strength class.
///
/// This only has an effect on Android. On other targets, any biometric strength
/// setting will enable all biometric authentication devices. See the [Android
/// documentation][android-docs] for more details.
///
/// [android-docs]: https://source.android.com/docs/security/features/biometric
#[derive(Debug)]
pub enum BiometricStrength {
    Strong,
    Weak,
}

/// A builder for conveniently defining a policy.
///
/// It is **highly recommended** to use the [`Self::new()`] (default) value
/// to create a policy with all options enabled, because each platform acts differently
/// when being requested to enable/disable various authentication methods.
/// Enabling all options is the safest way to ensure that the authentication prompt
/// will be displayed correctly on all platforms.
///
/// On Linux, at least one polkit action ID *MUST* be explicitly provided.
/// If not set (or empty), `build()` will return None.
#[derive(Debug)]
pub struct PolicyBuilder {
    inner: sys::PolicyBuilder,
}

impl Default for PolicyBuilder {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl PolicyBuilder {
    /// Returns a new policy with sane defaults.
    #[inline]
    pub const fn new() -> Self {
        Self {
            inner: sys::PolicyBuilder::new(),
        }
    }

    /// Sets the list of action identifiers that require authorization.
    ///
    /// On Linux systems, these map to polkit `action_id`s, which represent
    /// specific privileged operations (such as modifying system settings or
    /// powering off the device). The authentication backend uses the current
    /// action ID to determine whether the action is permitted and whether
    /// user authentication is required.
    ///
    /// The first entry is used as the default action ID. To switch between
    /// multiple action IDs at runtime on Linux, build a policy once and call
    /// [`Policy::set_action_id`] before each authentication. The setter
    /// validates against this list.
    ///
    /// This only has an effect on linux.
    #[inline]
    #[must_use]
    pub fn action_ids<I, S>(self, ids: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let ids = ids.into_iter().map(Into::into).collect::<Vec<String>>();
        Self {
            inner: self.inner.action_ids(ids),
        }
    }

    /// Configures biometric authentication with the given strength.
    ///
    /// The strength only has an effect on Android, see [`BiometricStrength`]
    /// for more details.
    #[inline]
    #[must_use]
    pub fn biometrics(self, strength: Option<BiometricStrength>) -> Self {
        Self {
            inner: self.inner.biometrics(strength),
        }
    }

    /// Sets whether the policy supports passwords.
    #[inline]
    #[must_use]
    pub fn password(self, password: bool) -> Self {
        Self {
            inner: self.inner.password(password),
        }
    }

    /// Sets whether the policy supports authentication via a proximity companion device, e.g., Apple Watch.
    ///
    /// This only has an effect on iOS and macOS.
    #[inline]
    #[must_use]
    pub fn companion(self, companion: bool) -> Self {
        Self {
            inner: self.inner.companion(companion),
        }
    }

    /// Sets whether the policy requires the companion device (Apple Watch) to be on the user's wrist.
    ///
    /// This only has an effect on Apple watchOS.
    #[inline]
    #[must_use]
    pub fn wrist_detection(self, wrist_detection: bool) -> Self {
        Self {
            inner: self.inner.wrist_detection(wrist_detection),
        }
    }

    /// Constructs the policy.
    ///
    /// Returns `None` if the specified configuration is not valid for the
    /// current target.
    #[inline]
    #[must_use]
    pub fn build(self) -> Option<Policy> {
        Some(Policy {
            inner: self.inner.build()?,
        })
    }
}

/// An authentication policy.
#[derive(Debug)]
pub struct Policy {
    inner: sys::Policy,
}

impl Policy {
    /// Sets the polkit action ID used on Linux.
    ///
    /// Returns [`Error::InvalidActionId`] if the ID is not in the allowed list
    /// provided via [`PolicyBuilder::action_ids`].
    ///
    /// This is a no-op on non-Linux platforms.
    #[inline]
    pub fn set_action_id(&mut self, id: impl Into<String>) -> Result<()> {
        self.inner.set_action_id(id.into())
    }
}