use {
anyhow::Result,
block2::RcBlock,
objc2::runtime::Bool,
objc2_foundation::{
NSDate,
NSDefaultRunLoopMode,
NSError,
NSInteger,
NSRunLoop,
NSString,
},
objc2_local_authentication::{
kLAErrorAuthenticationFailed,
kLAErrorBiometryLockout,
kLAErrorNotInteractive,
kLAErrorPasscodeNotSet,
kLAErrorSystemCancel,
kLAErrorUserCancel,
kLAErrorUserFallback,
LAContext,
LACredentialType,
LAPolicy,
},
std::{
sync::mpsc,
time::{
Duration,
Instant,
},
},
};
const POLICY: LAPolicy = LAPolicy::DeviceOwnerAuthentication;
pub(crate) fn authenticate(reason: &str) -> Result<()> {
if reason.is_empty() {
anyhow::bail!("System authentication reason must not be empty");
}
let context = new_context()?;
configure(&context, false)?;
ensure_evaluable(&context)?;
let reason = NSString::from_str(reason);
evaluate(&context, &reason, None)
}
fn new_context() -> Result<objc2::rc::Retained<LAContext>> {
Ok(unsafe { LAContext::new() })
}
fn configure(context: &LAContext, interaction_not_allowed: bool) -> Result<()> {
unsafe {
context.setTouchIDAuthenticationAllowableReuseDuration(0.0);
context.setInteractionNotAllowed(interaction_not_allowed);
context.setLocalizedFallbackTitle(Some(&*NSString::from_str("Enter Password")));
for credential_type in [LACredentialType::ApplicationPassword, LACredentialType::SmartCardPIN] {
context.setCredential_type(None, credential_type);
if context.isCredentialSet(credential_type) {
anyhow::bail!("System check refused to start with an injected credential");
}
}
}
Ok(())
}
fn ensure_evaluable(context: &LAContext) -> Result<()> {
match unsafe { context.canEvaluatePolicy_error(POLICY) } {
| Ok(()) => Ok(()),
| Err(error) => Err(error_from_code(error.code())),
}
}
fn evaluate(context: &LAContext, reason: &NSString, timeout: Option<Duration>) -> Result<()> {
let (sender, receiver) = mpsc::channel();
let reply = RcBlock::new(move |success: Bool, error: *mut NSError| {
let code = if error.is_null() {
None
} else {
Some(unsafe { &*error }.code())
};
let _ = sender.send(PromptResult {
success: success.as_bool(),
code,
});
});
unsafe {
context.evaluatePolicy_localizedReason_reply(POLICY, reason, &reply);
}
let started = Instant::now();
let run_loop = NSRunLoop::currentRunLoop();
loop {
if let Ok(result) = receiver.try_recv() {
return result.into_result();
}
if timeout.is_some_and(|timeout| started.elapsed() >= timeout) {
unsafe { context.invalidate() };
anyhow::bail!("System authentication timed out before a human completed it");
}
let until = NSDate::dateWithTimeIntervalSinceNow(0.05);
let mode = unsafe { NSDefaultRunLoopMode };
run_loop.runMode_beforeDate(mode, &until);
}
}
fn error_from_code(code: NSInteger) -> anyhow::Error {
let message = match code {
| code if code == kLAErrorUserCancel as NSInteger => "System authentication was cancelled",
| code if code == kLAErrorAuthenticationFailed as NSInteger => "System authentication failed",
| code if code == kLAErrorPasscodeNotSet as NSInteger => {
"The current user has no password, so system authentication cannot be performed"
},
| code if code == kLAErrorNotInteractive as NSInteger => "System authentication requires an interactive prompt",
| code if code == kLAErrorSystemCancel as NSInteger => "System authentication was cancelled by macOS",
| code if code == kLAErrorUserFallback as NSInteger => {
"System authentication did not accept the current user's password prompt"
},
| code if code == kLAErrorBiometryLockout as NSInteger => {
"Touch ID is locked; enter the current user's password in the prompt and try again"
},
| code => return anyhow::anyhow!("System authentication failed ({code})"),
};
anyhow::anyhow!(message)
}
struct PromptResult {
success: bool,
code: Option<NSInteger>,
}
impl PromptResult {
fn into_result(self) -> Result<()> {
if self.success {
return Ok(());
}
match self.code {
| Some(code) => Err(error_from_code(code)),
| None => anyhow::bail!("System authentication did not succeed"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_policy_fails_closed_without_a_human_prompt() -> Result<()> {
let context = new_context()?;
configure(&context, true)?;
let reason = NSString::from_str("unlock profile test");
let result = evaluate(&context, &reason, Some(Duration::from_secs(2)));
assert!(
result.is_err(),
"system authentication succeeded without a human prompt: {result:?}"
);
Ok(())
}
}