Skip to content

Commit 5f374f8

Browse files
authored
Merge pull request #4 from Choochmeque/macos-support
Macos support
2 parents 953d01a + 2dbe08f commit 5f374f8

File tree

6 files changed

+281
-24
lines changed

6 files changed

+281
-24
lines changed

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ serde_repr = "0.1"
1818
thiserror = "2"
1919
log = "0.4"
2020

21+
[target.'cfg(target_os = "macos")'.dependencies]
22+
objc2 = "0.5"
23+
objc2-foundation = "0.2"
24+
objc2-local-authentication = { version = "0.2", features = ["LAContext", "LAError", "block2"] }
25+
block2 = "0.5"
26+
2127
[target.'cfg(target_os = "windows")'.dependencies]
2228
windows = { version = "0.58", features = ["Foundation", "Security_Credentials_UI", "Win32_Foundation", "Win32_UI_WindowsAndMessaging"] }
2329

src/desktop.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,37 @@ pub struct Biometry<R: Runtime>(AppHandle<R>);
1616
impl<R: Runtime> Biometry<R> {
1717
pub fn status(&self) -> crate::Result<Status> {
1818
Err(crate::Error::from(std::io::Error::other(
19-
"Biometry is not supported on desktop platforms",
19+
"Biometry is not supported on this platform",
2020
)))
2121
}
2222

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

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

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

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

4747
pub fn remove_data(&self, _options: RemoveDataOptions) -> crate::Result<()> {
4848
Err(crate::Error::from(std::io::Error::other(
49-
"Biometry is not supported on desktop platforms",
49+
"Biometry is not supported on this platform",
5050
)))
5151
}
5252
}

src/error.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,60 @@ use serde::{ser::Serializer, Serialize};
22

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

5+
/// Replica of the tauri::plugin::mobile::ErrorResponse for desktop platforms.
6+
#[cfg(desktop)]
7+
#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)]
8+
pub struct ErrorResponse<T = ()> {
9+
/// Error code.
10+
pub code: Option<String>,
11+
/// Error message.
12+
pub message: Option<String>,
13+
/// Optional error data.
14+
#[serde(flatten)]
15+
pub data: T,
16+
}
17+
18+
#[cfg(desktop)]
19+
impl<T> std::fmt::Display for ErrorResponse<T> {
20+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21+
if let Some(code) = &self.code {
22+
write!(f, "[{code}]")?;
23+
if self.message.is_some() {
24+
write!(f, " - ")?;
25+
}
26+
}
27+
if let Some(message) = &self.message {
28+
write!(f, "{message}")?;
29+
}
30+
Ok(())
31+
}
32+
}
33+
34+
/// Replica of the tauri::plugin::mobile::PluginInvokeError for desktop platforms.
35+
#[cfg(desktop)]
36+
#[derive(Debug, thiserror::Error)]
37+
pub enum PluginInvokeError {
38+
/// Error returned from direct desktop plugin.
39+
#[error(transparent)]
40+
InvokeRejected(#[from] ErrorResponse),
41+
/// Failed to deserialize response.
42+
#[error("failed to deserialize response: {0}")]
43+
CannotDeserializeResponse(serde_json::Error),
44+
/// Failed to serialize request payload.
45+
#[error("failed to serialize payload: {0}")]
46+
CannotSerializePayload(serde_json::Error),
47+
}
48+
549
#[derive(Debug, thiserror::Error)]
650
pub enum Error {
751
#[error(transparent)]
852
Io(#[from] std::io::Error),
953
#[cfg(mobile)]
1054
#[error(transparent)]
1155
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
56+
#[cfg(desktop)]
57+
#[error(transparent)]
58+
PluginInvoke(#[from] crate::error::PluginInvokeError),
1259
}
1360

1461
impl Serialize for Error {

src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ use tauri::{
55

66
pub use models::*;
77

8-
#[cfg(all(desktop, not(target_os = "windows")))]
8+
#[cfg(all(desktop, not(target_os = "windows"), not(target_os = "macos")))]
99
mod desktop;
10+
#[cfg(target_os = "macos")]
11+
mod macos;
1012
#[cfg(mobile)]
1113
mod mobile;
1214
#[cfg(target_os = "windows")]
@@ -18,8 +20,10 @@ mod models;
1820

1921
pub use error::{Error, Result};
2022

21-
#[cfg(all(desktop, not(target_os = "windows")))]
23+
#[cfg(all(desktop, not(target_os = "windows"), not(target_os = "macos")))]
2224
use desktop::Biometry;
25+
#[cfg(target_os = "macos")]
26+
use macos::Biometry;
2327
#[cfg(mobile)]
2428
use mobile::Biometry;
2529
#[cfg(target_os = "windows")]
@@ -50,10 +54,12 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
5054
.setup(|app, api| {
5155
#[cfg(mobile)]
5256
let biometry = mobile::init(app, api)?;
53-
#[cfg(all(desktop, not(target_os = "windows")))]
57+
#[cfg(all(desktop, not(target_os = "windows"), not(target_os = "macos")))]
5458
let biometry = desktop::init(app, api)?;
5559
#[cfg(target_os = "windows")]
5660
let biometry = windows::init(app, api)?;
61+
#[cfg(target_os = "macos")]
62+
let biometry = macos::init(app, api)?;
5763
app.manage(biometry);
5864
Ok(())
5965
})

src/macos.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use objc2_local_authentication::{LABiometryType, LAContext, LAError, LAPolicy};
2+
use serde::de::DeserializeOwned;
3+
use tauri::{plugin::PluginApi, AppHandle, Runtime};
4+
5+
use crate::models::*;
6+
7+
pub fn init<R: Runtime, C: DeserializeOwned>(
8+
app: &AppHandle<R>,
9+
_api: PluginApi<R, C>,
10+
) -> crate::Result<Biometry<R>> {
11+
Ok(Biometry(app.clone()))
12+
}
13+
14+
fn la_error_to_string(error: LAError) -> &'static str {
15+
match error {
16+
LAError::AppCancel => "appCancel",
17+
LAError::AuthenticationFailed => "authenticationFailed",
18+
LAError::InvalidContext => "invalidContext",
19+
LAError::NotInteractive => "notInteractive",
20+
LAError::PasscodeNotSet => "passcodeNotSet",
21+
LAError::SystemCancel => "systemCancel",
22+
LAError::UserCancel => "userCancel",
23+
LAError::UserFallback => "userFallback",
24+
LAError::BiometryLockout => "biometryLockout",
25+
LAError::BiometryNotAvailable => "biometryNotAvailable",
26+
LAError::BiometryNotEnrolled => "biometryNotEnrolled",
27+
_ => "unknown",
28+
}
29+
}
30+
31+
/// Access to the biometry APIs.
32+
pub struct Biometry<R: Runtime>(AppHandle<R>);
33+
34+
impl<R: Runtime> Biometry<R> {
35+
pub fn status(&self) -> crate::Result<Status> {
36+
let context = unsafe { LAContext::new() };
37+
38+
let can_evaluate = unsafe {
39+
context.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthenticationWithBiometrics)
40+
};
41+
42+
let biometry_type = unsafe { context.biometryType() };
43+
44+
let is_available = can_evaluate.is_ok();
45+
let mut error_reason: Option<String> = None;
46+
let mut error_code: Option<String> = None;
47+
48+
if let Err(error) = can_evaluate {
49+
let ns_error = &*error;
50+
51+
// Get error description
52+
let description = ns_error.localizedDescription();
53+
error_reason = Some(description.to_string());
54+
55+
// Map error code to string representation
56+
let code = LAError(ns_error.code());
57+
error_code = Some(la_error_to_string(code).to_string());
58+
}
59+
60+
// Map LABiometryType to our BiometryType enum
61+
let mapped_biometry_type = match biometry_type {
62+
LABiometryType::None => BiometryType::None,
63+
LABiometryType::TouchID => BiometryType::TouchID,
64+
LABiometryType::FaceID => BiometryType::FaceID,
65+
#[allow(unreachable_patterns)]
66+
_ => BiometryType::None,
67+
};
68+
69+
Ok(Status {
70+
is_available,
71+
biometry_type: mapped_biometry_type,
72+
error: error_reason,
73+
error_code,
74+
})
75+
}
76+
77+
pub fn authenticate(&self, reason: String, options: AuthOptions) -> crate::Result<()> {
78+
let context = unsafe { LAContext::new() };
79+
80+
// Check if biometry is available or device credential is allowed
81+
let can_evaluate_biometry = unsafe {
82+
context.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthenticationWithBiometrics)
83+
};
84+
85+
let allow_device_credential = options.allow_device_credential.unwrap_or(false);
86+
87+
if can_evaluate_biometry.is_err() && !allow_device_credential {
88+
// Biometry unavailable and fallback disabled
89+
if let Err(error) = can_evaluate_biometry {
90+
let ns_error = &*error;
91+
let description = ns_error.localizedDescription();
92+
let code = LAError(ns_error.code());
93+
let error_code = la_error_to_string(code);
94+
95+
return Err(crate::Error::Io(std::io::Error::new(
96+
std::io::ErrorKind::PermissionDenied,
97+
format!("{error_code}: {description}"),
98+
)));
99+
}
100+
}
101+
102+
// Set localized titles if provided
103+
if let Some(fallback_title) = options.fallback_title {
104+
unsafe {
105+
let title_str = objc2_foundation::NSString::from_str(&fallback_title);
106+
context.setLocalizedFallbackTitle(Some(&title_str));
107+
}
108+
}
109+
110+
if let Some(cancel_title) = options.cancel_title {
111+
unsafe {
112+
let title_str = objc2_foundation::NSString::from_str(&cancel_title);
113+
context.setLocalizedCancelTitle(Some(&title_str));
114+
}
115+
}
116+
117+
// Set authentication reuse duration to 0 (no reuse)
118+
unsafe {
119+
context.setTouchIDAuthenticationAllowableReuseDuration(0.0);
120+
}
121+
122+
// Determine which policy to use
123+
let policy = if allow_device_credential {
124+
LAPolicy::DeviceOwnerAuthentication
125+
} else {
126+
LAPolicy::DeviceOwnerAuthenticationWithBiometrics
127+
};
128+
129+
// Create a channel to communicate between the callback and the main thread
130+
let (tx, rx) = std::sync::mpsc::channel();
131+
132+
// Perform authentication
133+
unsafe {
134+
let reason_str = objc2_foundation::NSString::from_str(&reason);
135+
let tx_clone = tx.clone();
136+
137+
context.evaluatePolicy_localizedReason_reply(
138+
policy,
139+
&reason_str,
140+
&block2::StackBlock::new(
141+
move |success: objc2::runtime::Bool,
142+
error_ptr: *mut objc2_foundation::NSError| {
143+
if success.as_bool() {
144+
let _ = tx_clone.send(Ok(()));
145+
} else if !error_ptr.is_null() {
146+
let error = &*error_ptr;
147+
let description = error.localizedDescription().to_string();
148+
let code = LAError(error.code());
149+
let error_code = la_error_to_string(code);
150+
151+
let _ = tx_clone.send(Err(crate::Error::Io(std::io::Error::new(
152+
std::io::ErrorKind::PermissionDenied,
153+
format!("{error_code}: {description}"),
154+
))));
155+
} else {
156+
let _ = tx_clone.send(Err(crate::Error::Io(std::io::Error::new(
157+
std::io::ErrorKind::PermissionDenied,
158+
"authenticationFailed: Unknown error".to_string(),
159+
))));
160+
}
161+
},
162+
),
163+
);
164+
}
165+
166+
// Wait for authentication result
167+
match rx.recv() {
168+
Ok(result) => result,
169+
Err(_) => Err(crate::Error::Io(std::io::Error::other(
170+
"authenticationFailed: Failed to receive authentication result",
171+
))),
172+
}
173+
}
174+
175+
pub fn has_data(&self, _options: DataOptions) -> crate::Result<bool> {
176+
Err(crate::Error::from(std::io::Error::other(
177+
"Biometry has_data is not yet implemented on macOS",
178+
)))
179+
}
180+
181+
pub fn get_data(&self, _options: GetDataOptions) -> crate::Result<DataResponse> {
182+
Err(crate::Error::from(std::io::Error::other(
183+
"Biometry get_data is not yet implemented on macOS",
184+
)))
185+
}
186+
187+
pub fn set_data(&self, _options: SetDataOptions) -> crate::Result<()> {
188+
Err(crate::Error::from(std::io::Error::other(
189+
"Biometry set_data is not yet implemented on macOS",
190+
)))
191+
}
192+
193+
pub fn remove_data(&self, _options: RemoveDataOptions) -> crate::Result<()> {
194+
Err(crate::Error::from(std::io::Error::other(
195+
"Biometry remove_data is not yet implemented on macOS",
196+
)))
197+
}
198+
}

0 commit comments

Comments
 (0)