Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
998 changes: 642 additions & 356 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,8 @@ objc2-ui-kit = { version = "0.3.1", default-features = false, feat


## Linux-specific dependencies used in multiple crates in the workspace.
polkit = "=0.17.0"
gio = "=0.17.0"

zbus = "5.12.0"
zbus_polkit = "5.0.0"

## Windows-specific dependencies used in multiple crates in the workspace.
windows-core = { version = "0.56.0", default-features = false }
Expand Down
5 changes: 3 additions & 2 deletions crates/authentication/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition.workspace = true
authors = [
"Kevin Boos <kevinaboos@gmail.com>",
"Klim Tsoutsman <klim@tsoutsman.com>",
"Tyrese Luo <tyreseluo@outlook.com>",
"Project Robius Maintainers",
]
description = "Rust abstractions for multi-platform native authentication: biometrics, fingerprint, password, screen lock, TouchID, FaceID, Windows Hello, etc."
Expand Down Expand Up @@ -50,8 +51,8 @@ objc2-foundation = { workspace = true, features = ["NSError", "NSString"] }
# optional = true

[target.'cfg(target_os = "linux")'.dependencies]
polkit.workspace = true
gio.workspace = true
zbus.workspace = true
zbus_polkit = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
retry.workspace = true
Expand Down
104 changes: 67 additions & 37 deletions crates/authentication/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,43 +30,73 @@ To use this crate on Android, you must add the following to your app's `AndroidM
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
```

## Example

```rust
use robius_authentication::{
AndroidText, BiometricStrength, Context, Policy, PolicyBuilder, Text, WindowsText,
};

let policy: Policy = PolicyBuilder::new()
.biometrics(Some(BiometricStrength::Strong))
.password(true)
.companion(true)
.build()
.unwrap();

let text = Text {
android: AndroidText {
title: "Title",
subtitle: None,
description: None,
},
apple: "authenticate",
windows: WindowsText::new("Title", "Description"),
};

let callback = |auth_result| {
match auth_result {
Ok(_) => log::info!("Authentication success!"),
Err(_) => log::error!(Authentication failed!"),
}
};

Context::new(())
.authenticate(text, &policy, callback)
.expect("Authentication failed");
## Usage on Linux

On Linux, `robius-authentication` uses **polkit** to request authorization via the
desktop environment's native authentication prompt (GNOME/KDE/etc).

> [!IMPORTANT]
> **Ensure a polkit agent is running**
>
> The prompt is displayed by a polkit authentication agent (GNOME/KDE usually start one automatically).
> If no agent is running (headless/SSH), no prompt will appear and auth will fail.

### Write policy file.

You can create your application's own policy file from scratch, or also create one from a template policy file.

See here for an example template policy file: [`./examples/org.robius.authentication.policy`](./examples/org.robius.authentication.policy)

> [!NOTE]
>
> A polkit policy file (`*.policy`) is an XML file that defines one or more authorization **actions** for your application.
polkit uses these definitions to determine whether a user is allowed to perform privileged operations.
>
> The “actions” directory (/usr/share/polkit-1/actions/) contains .policy files that define polkit authorization actions.

### Quick Test Mode ⚠️

#### Step 1. Install your policy file (`*.policy`)

Install the policy file into the polkit `actions` directory, which is used to define authorization actions for the application.

Manually execute the following command:

```bash
sudo install -Dm644 com.yourapp.policy /usr/share/polkit-1/actions/
```
`polkit` loads policy definitions from /usr/share/polkit-1/actions/.

#### Step 2. Ensure your policy file was correctly installed.

```bash
pkaction --action-id <YOUR_POLICY_File_ACTION_ID>
```

For more details about the prompt text, see the `Text` struct,
which allows you to customize the prompt for each platform.
> During the test mode, you don't need to worry about the location of the policy file; just ensure it installs correctly.

### Release Mode

> The official polkit documentation explicitly states: Mechanisms should install action XML files to [/usr/share/polkit-1/actions](https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html).


As long as your packaging tool provides the capability to automatically install *.policy files under /usr/share/polkit-1/actions/.

This document provides example for when you are using `cargo-packager`.

See the example below for use `cargo-packager`.

#### Use `cargo-packager`

```toml
# https://docs.crabnebula.dev/packager/configuration/#debianconfig
[package.metadata.packager.deb]
depends = "./dist/depends_deb.txt"
desktop_template = "./packaging/robrix.desktop"
section = "utils"

[package.metadata.packager.deb.files]
"./packaging/org.robius.authentication.policy" = "/usr/share/polkit-1/actions/org.robius.authentication.policy"
```

[`polkit`]: https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html
When you are packaging, `cargo-packager` automatically installs files to their target direactory.
37 changes: 37 additions & 0 deletions crates/authentication/examples/org.robius.authentication.policy
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Template Policy File -->
<policyconfig>
<!-- Optional metadata. Some agents use these for display. -->
<vendor>Your vendor name</vendor>
<vendor_url>Your vendor url</vendor_url>
<!-- Icon name from the icon theme (e.g., /usr/share/icons). If missing, a default icon is shown. -->
<icon_name>Your icon name</icon_name>

<!-- ⚠️ Replace action id -->
<action id="org.robius.authentication">
<description>Authenticate to use YourApp</description>
<!-- The main authentication message -->
<message>Authentication is required</message>
<defaults>
<!-- Equivalent to: implicit any: no -->
<allow_any>no</allow_any>
<!-- Equivalent to: implicit inactive: no -->
<allow_inactive>no</allow_inactive>
<!-- Equivalent to: implicit active: auth_admin_keep -->
<allow_active>auth_admin</allow_active>
</defaults>
</action>

<!-- ⚠️ Replace action id -->
<action id="org.robius.authentication.settings">
<description>Authenticate to change settings</description>
<message>Authentication is required to change settings.</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>

<!-- Define your more actions -->
</policyconfig>
90 changes: 69 additions & 21 deletions crates/authentication/examples/simple_authentication.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
//! # Recommended usage
//!
//! `Context::authenticate()` is asynchronous: it returns immediately after *starting*
//! authentication and delivers the final result via the callback.
//!
//! **In GUI applications**, you typically do not need any additional synchronization.
//! The application's event loop keeps the process alive while the user interacts with
//! the authentication prompt.
//!
//! ```no_run
//! let context = Context::new(());
//! context.authenticate(TEXT, &POLICY, |result| {
//! match result {
//! Ok(()) => println!("Authentication successful"),
//! Err(e) => eprintln!("Authentication failed: {:?}", e),
//! }
//! });
//! ```
//!
//! # Why this example uses `mpsc::channel()`
//!
//! This file is a CLI-style demo. Without an event loop, `main()` would exit before the
//! authentication completes (especially on Linux where the polkit check runs in the
//! background). We therefore use an `mpsc::channel()` to wait for the callback.
//!
//! # Linux notes
//!
//! - Ensure the polkit policy file is installed for the chosen `action_id`
//! (see README: "Usage on Linux").
//! - Ensure a polkit authentication agent is running in the current desktop session,
//! otherwise no prompt will appear and the request will fail with `NoAgent`/`Unavailable`.

use std::sync::mpsc;

use robius_authentication::{
AndroidText, BiometricStrength, Context, Policy, PolicyBuilder, Text, WindowsText,
AndroidText, BiometricStrength, Context, PolicyBuilder, Text, WindowsText,
};

const POLICY: Policy = PolicyBuilder::new()
.biometrics(Some(BiometricStrength::Strong))
.password(true)
.companion(true)
.build()
.unwrap();

const TEXT: Text = Text {
android: AndroidText {
title: "Title",
Expand All @@ -21,20 +48,41 @@ const TEXT: Text = Text {

fn main() {
let context = Context::new(());
let mut policy = PolicyBuilder::new()
// On Linux You need to set action_ids.
// See: ./org.robius.authentication.policy file settings and (README: "Usage on Linux").
.action_ids([
"org.robius.authentication",
"org.robius.authentication.settings",
])
.biometrics(Some(BiometricStrength::Strong))
.password(true)
.companion(true)
.build()
.unwrap();

if let Err(e) = policy.set_action_id("org.robius.authentication.settings") {
eprintln!("Invalid action_id: {:?}", e);
return;
}

let (tx, rx) = mpsc::channel();

// Start authentication. `res` only indicates whether the request was successfully
// initiated; the final result is delivered via the callback.
let res = context.authenticate(TEXT, &policy, move |result| {
let _ = tx.send(result);
});

let res = context.authenticate(
TEXT,
&POLICY,
|result| match result {
Ok(_) => println!("Authentication successful"),
Err(e) => println!("Authentication failed: {:?}", e),
},
);

// Note: if `res` is `Ok`, the authentication did not necessarily succeed.
// The callback will be called with the result of the authentication.
// If `res` is `Err`, it indicates an error in the authentication policy or context setup.
if let Err(e) = res {
eprintln!("Authentication failed: {:?}", e);
eprintln!("Failed to start authentication: {:?}", e);
return;
}

// Block until the callback produces a result.
match rx.recv() {
Ok(Ok(_)) => println!("Authentication successful"),
Ok(Err(e)) => println!("Authentication failed: {:?}", e),
Err(e) => eprintln!("Failed to receive auth result: {:?}", e),
}
}
Binary file added crates/authentication/img/linux-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion crates/authentication/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pub type Result<T> = std::result::Result<T, Error>;

/// An error produced during authentication.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Error {
// TODO: Reexport jni::errors::Error
// TODO: Remove target cfg
Expand All @@ -19,6 +19,8 @@ pub enum Error {
Unavailable,
/// The user canceled authentication.
UserCanceled,
/// The provided action ID is not in the policy's allowed list.
InvalidActionId,

// Apple-specific errors
/// The app canceled authentication.
Expand Down
Loading