use std::sync::mpsc;
use std::time::Duration;
use block2::RcBlock;
use objc2::runtime::Bool;
use objc2_foundation::{NSError, NSString};
use objc2_local_authentication::{LAContext, LAPolicy};
use super::{
classify_preflight_error, classify_reply_error, classify_session, AuthOutcome, AuthPolicy,
Authenticator, LaError,
};
const PROMPT_TIMEOUT: Duration = Duration::from_secs(120);
const CALLER_SECURITY_SESSION: u32 = u32::MAX;
#[link(name = "Security", kind = "framework")]
extern "C" {
fn SessionGetInfo(session: u32, session_id: *mut u32, attributes: *mut u32) -> i32;
}
fn caller_session_attributes() -> Result<u32, i32> {
let mut session_id = 0u32;
let mut attributes = 0u32;
#[allow(unsafe_code)]
let status = unsafe {
SessionGetInfo(
CALLER_SECURITY_SESSION,
&raw mut session_id,
&raw mut attributes,
)
};
if status == 0 {
Ok(attributes)
} else {
Err(status)
}
}
fn la_error(err: &NSError) -> LaError {
LaError {
domain: err.domain().to_string(),
code: err.code(),
description: err.localizedDescription().to_string(),
}
}
pub(crate) struct LocalAuthenticator;
impl Authenticator for LocalAuthenticator {
fn authenticate(&self, reason: &str, policy: AuthPolicy) -> AuthOutcome {
if let Some(outcome) = classify_session(caller_session_attributes()) {
return outcome;
}
let la_policy = match policy {
AuthPolicy::DeviceOwner => LAPolicy::DeviceOwnerAuthentication,
AuthPolicy::BiometricsOnly => LAPolicy::DeviceOwnerAuthenticationWithBiometrics,
};
#[allow(unsafe_code)]
let ctx = unsafe { LAContext::new() };
#[allow(unsafe_code)]
let preflight = unsafe { ctx.canEvaluatePolicy_error(la_policy) };
if let Err(err) = preflight {
return classify_preflight_error(la_error(&err));
}
let (tx, rx) = mpsc::channel::<(bool, Option<LaError>)>();
let reply = RcBlock::new(move |success: Bool, error: *mut NSError| {
let success = success.as_bool();
let error_desc = if error.is_null() {
None
} else {
#[allow(unsafe_code)]
let error = unsafe { &*error };
Some(la_error(error))
};
let _ = tx.send((success, error_desc));
});
let reason = NSString::from_str(reason);
#[allow(unsafe_code)]
unsafe {
ctx.evaluatePolicy_localizedReason_reply(la_policy, &reason, &reply);
}
match rx.recv_timeout(PROMPT_TIMEOUT) {
Ok((true, _)) => AuthOutcome::Authorized,
Ok((false, error)) => classify_reply_error(error),
Err(_) => {
#[allow(unsafe_code)]
unsafe {
ctx.invalidate();
}
AuthOutcome::Denied(format!(
"timed out after {}s waiting for a response to the authentication prompt",
PROMPT_TIMEOUT.as_secs()
))
}
}
}
}