Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ serde_repr = "0.1"
thiserror = "2"
log = "0.4"

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.5"
objc2-foundation = "0.2"
objc2-local-authentication = { version = "0.2", features = ["LAContext", "LAError", "block2"] }
block2 = "0.5"

[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = ["Foundation", "Security_Credentials_UI", "Win32_Foundation", "Win32_UI_WindowsAndMessaging"] }

Expand Down
12 changes: 6 additions & 6 deletions src/desktop.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};

Check warning on line 2 in src/desktop.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)

use crate::models::*;

Expand All @@ -16,37 +16,37 @@
impl<R: Runtime> Biometry<R> {
pub fn status(&self) -> crate::Result<Status> {
Err(crate::Error::from(std::io::Error::other(
"Biometry is not supported on desktop platforms",
"Biometry is not supported on this platform",
)))
}

pub fn authenticate(&self, _reason: String, _options: AuthOptions) -> crate::Result<()> {
Err(crate::Error::from(std::io::Error::other(
"Biometry is not supported on desktop platforms",
"Biometry is not supported on this platform",
)))
}

pub fn has_data(&self, _options: DataOptions) -> crate::Result<bool> {
Err(crate::Error::from(std::io::Error::other(
"Biometry is not supported on desktop platforms",
"Biometry is not supported on this platform",
)))
}

pub fn get_data(&self, _options: GetDataOptions) -> crate::Result<DataResponse> {
Err(crate::Error::from(std::io::Error::other(
"Biometry is not supported on desktop platforms",
"Biometry is not supported on this platform",
)))
}

pub fn set_data(&self, _options: SetDataOptions) -> crate::Result<()> {
Err(crate::Error::from(std::io::Error::other(
"Biometry is not supported on desktop platforms",
"Biometry is not supported on this platform",
)))
}

pub fn remove_data(&self, _options: RemoveDataOptions) -> crate::Result<()> {
Err(crate::Error::from(std::io::Error::other(
"Biometry is not supported on desktop platforms",
"Biometry is not supported on this platform",
)))
}
}
47 changes: 47 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,60 @@

pub type Result<T> = std::result::Result<T, Error>;

/// Replica of the tauri::plugin::mobile::ErrorResponse for desktop platforms.

Check warning on line 5 in src/error.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)
#[cfg(desktop)]
#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)]

Check warning on line 7 in src/error.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (thiserror)
pub struct ErrorResponse<T = ()> {
/// Error code.
pub code: Option<String>,
/// Error message.
pub message: Option<String>,
/// Optional error data.
#[serde(flatten)]
pub data: T,
}

#[cfg(desktop)]
impl<T> std::fmt::Display for ErrorResponse<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(code) = &self.code {
write!(f, "[{code}]")?;
if self.message.is_some() {
write!(f, " - ")?;
}
}
if let Some(message) = &self.message {
write!(f, "{message}")?;
}
Ok(())
}
}

/// Replica of the tauri::plugin::mobile::PluginInvokeError for desktop platforms.

Check warning on line 34 in src/error.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)
#[cfg(desktop)]
#[derive(Debug, thiserror::Error)]

Check warning on line 36 in src/error.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (thiserror)
pub enum PluginInvokeError {
/// Error returned from direct desktop plugin.
#[error(transparent)]
InvokeRejected(#[from] ErrorResponse),
/// Failed to deserialize response.
#[error("failed to deserialize response: {0}")]
CannotDeserializeResponse(serde_json::Error),
/// Failed to serialize request payload.
#[error("failed to serialize payload: {0}")]
CannotSerializePayload(serde_json::Error),
}

#[derive(Debug, thiserror::Error)]

Check warning on line 49 in src/error.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (thiserror)
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),

Check warning on line 55 in src/error.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)
#[cfg(desktop)]
#[error(transparent)]
PluginInvoke(#[from] crate::error::PluginInvokeError),
}

impl Serialize for Error {
Expand Down
12 changes: 9 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use tauri::{

Check warning on line 1 in src/lib.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)
plugin::{Builder, TauriPlugin},

Check warning on line 2 in src/lib.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (Tauri)
Manager, Runtime,
};

pub use models::*;

#[cfg(all(desktop, not(target_os = "windows")))]
#[cfg(all(desktop, not(target_os = "windows"), not(target_os = "macos")))]
mod desktop;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(mobile)]
mod mobile;
#[cfg(target_os = "windows")]
Expand All @@ -18,14 +20,16 @@

pub use error::{Error, Result};

#[cfg(all(desktop, not(target_os = "windows")))]
#[cfg(all(desktop, not(target_os = "windows"), not(target_os = "macos")))]
use desktop::Biometry;
#[cfg(target_os = "macos")]
use macos::Biometry;
#[cfg(mobile)]
use mobile::Biometry;
#[cfg(target_os = "windows")]
use windows::Biometry;

/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the biometry APIs.

Check warning on line 32 in src/lib.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)

Check warning on line 32 in src/lib.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)

Check warning on line 32 in src/lib.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)
pub trait BiometryExt<R: Runtime> {
fn biometry(&self) -> &Biometry<R>;
}
Expand All @@ -37,7 +41,7 @@
}

/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {

Check warning on line 44 in src/lib.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (Tauri)
Builder::new("biometry")
.invoke_handler(tauri::generate_handler![
commands::status,
Expand All @@ -50,10 +54,12 @@
.setup(|app, api| {
#[cfg(mobile)]
let biometry = mobile::init(app, api)?;
#[cfg(all(desktop, not(target_os = "windows")))]
#[cfg(all(desktop, not(target_os = "windows"), not(target_os = "macos")))]
let biometry = desktop::init(app, api)?;
#[cfg(target_os = "windows")]
let biometry = windows::init(app, api)?;
#[cfg(target_os = "macos")]
let biometry = macos::init(app, api)?;
app.manage(biometry);
Ok(())
})
Expand Down
198 changes: 198 additions & 0 deletions src/macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use objc2_local_authentication::{LABiometryType, LAContext, LAError, LAPolicy};

Check warning on line 1 in src/macos.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (objc)
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};

Check warning on line 3 in src/macos.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (tauri)

use crate::models::*;

pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<Biometry<R>> {
Ok(Biometry(app.clone()))
}

fn la_error_to_string(error: LAError) -> &'static str {
match error {
LAError::AppCancel => "appCancel",
LAError::AuthenticationFailed => "authenticationFailed",
LAError::InvalidContext => "invalidContext",
LAError::NotInteractive => "notInteractive",
LAError::PasscodeNotSet => "passcodeNotSet",

Check warning on line 20 in src/macos.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (Passcode)
LAError::SystemCancel => "systemCancel",
LAError::UserCancel => "userCancel",
LAError::UserFallback => "userFallback",
LAError::BiometryLockout => "biometryLockout",
LAError::BiometryNotAvailable => "biometryNotAvailable",
LAError::BiometryNotEnrolled => "biometryNotEnrolled",
_ => "unknown",
}
}

/// Access to the biometry APIs.
pub struct Biometry<R: Runtime>(AppHandle<R>);

impl<R: Runtime> Biometry<R> {
pub fn status(&self) -> crate::Result<Status> {
let context = unsafe { LAContext::new() };

let can_evaluate = unsafe {
context.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthenticationWithBiometrics)
};

let biometry_type = unsafe { context.biometryType() };

let is_available = can_evaluate.is_ok();
let mut error_reason: Option<String> = None;
let mut error_code: Option<String> = None;

if let Err(error) = can_evaluate {
let ns_error = &*error;

// Get error description
let description = ns_error.localizedDescription();
error_reason = Some(description.to_string());

// Map error code to string representation
let code = LAError(ns_error.code());
error_code = Some(la_error_to_string(code).to_string());
}

// Map LABiometryType to our BiometryType enum
let mapped_biometry_type = match biometry_type {
LABiometryType::None => BiometryType::None,
LABiometryType::TouchID => BiometryType::TouchID,
LABiometryType::FaceID => BiometryType::FaceID,
#[allow(unreachable_patterns)]
_ => BiometryType::None,
};

Ok(Status {
is_available,
biometry_type: mapped_biometry_type,
error: error_reason,
error_code,
})
}

pub fn authenticate(&self, reason: String, options: AuthOptions) -> crate::Result<()> {
let context = unsafe { LAContext::new() };

// Check if biometry is available or device credential is allowed
let can_evaluate_biometry = unsafe {
context.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthenticationWithBiometrics)
};

let allow_device_credential = options.allow_device_credential.unwrap_or(false);

if can_evaluate_biometry.is_err() && !allow_device_credential {
// Biometry unavailable and fallback disabled
if let Err(error) = can_evaluate_biometry {
let ns_error = &*error;
let description = ns_error.localizedDescription();
let code = LAError(ns_error.code());
let error_code = la_error_to_string(code);

return Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("{error_code}: {description}"),
)));
}
}

// Set localized titles if provided
if let Some(fallback_title) = options.fallback_title {
unsafe {
let title_str = objc2_foundation::NSString::from_str(&fallback_title);
context.setLocalizedFallbackTitle(Some(&title_str));
}
}

if let Some(cancel_title) = options.cancel_title {
unsafe {
let title_str = objc2_foundation::NSString::from_str(&cancel_title);
context.setLocalizedCancelTitle(Some(&title_str));
}
}

// Set authentication reuse duration to 0 (no reuse)
unsafe {
context.setTouchIDAuthenticationAllowableReuseDuration(0.0);
}

// Determine which policy to use
let policy = if allow_device_credential {
LAPolicy::DeviceOwnerAuthentication
} else {
LAPolicy::DeviceOwnerAuthenticationWithBiometrics
};

// Create a channel to communicate between the callback and the main thread
let (tx, rx) = std::sync::mpsc::channel();

// Perform authentication
unsafe {
let reason_str = objc2_foundation::NSString::from_str(&reason);
let tx_clone = tx.clone();

context.evaluatePolicy_localizedReason_reply(
policy,
&reason_str,
&block2::StackBlock::new(
move |success: objc2::runtime::Bool,
error_ptr: *mut objc2_foundation::NSError| {
if success.as_bool() {
let _ = tx_clone.send(Ok(()));
} else if !error_ptr.is_null() {
let error = &*error_ptr;
let description = error.localizedDescription().to_string();
let code = LAError(error.code());
let error_code = la_error_to_string(code);

let _ = tx_clone.send(Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("{error_code}: {description}"),
))));
} else {
let _ = tx_clone.send(Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"authenticationFailed: Unknown error".to_string(),
))));
}
},
),
);
}

// Wait for authentication result
match rx.recv() {
Ok(result) => result,
Err(_) => Err(crate::Error::Io(std::io::Error::other(
"authenticationFailed: Failed to receive authentication result",
))),
}
}

pub fn has_data(&self, _options: DataOptions) -> crate::Result<bool> {
Err(crate::Error::from(std::io::Error::other(
"Biometry has_data is not yet implemented on macOS",
)))
}

pub fn get_data(&self, _options: GetDataOptions) -> crate::Result<DataResponse> {
Err(crate::Error::from(std::io::Error::other(
"Biometry get_data is not yet implemented on macOS",
)))
}

pub fn set_data(&self, _options: SetDataOptions) -> crate::Result<()> {
Err(crate::Error::from(std::io::Error::other(
"Biometry set_data is not yet implemented on macOS",
)))
}

pub fn remove_data(&self, _options: RemoveDataOptions) -> crate::Result<()> {
Err(crate::Error::from(std::io::Error::other(
"Biometry remove_data is not yet implemented on macOS",
)))
}
}
Loading
Loading