#[cfg(all(unix, feature = "libusb"))]
use std::collections::HashMap;
#[cfg(all(unix, feature = "libusb"))]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(all(unix, feature = "libusb"))]
use std::sync::{Arc, Mutex, RwLock};
#[cfg(all(unix, feature = "libusb"))]
use std::time::Duration;
#[cfg(all(unix, feature = "libusb"))]
use tokio::sync::oneshot;
#[cfg(all(unix, feature = "libusb"))]
use tracing::{info, warn};
#[cfg(all(unix, feature = "libusb"))]
pub type ApprovalId = u64;
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalDecision {
Approved,
Denied,
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationType {
Register,
Authenticate,
}
#[cfg(all(unix, feature = "libusb"))]
impl OperationType {
pub fn event_id(&self) -> &'static str {
match self {
OperationType::Register => "fido2_register_request",
OperationType::Authenticate => "fido2_authenticate_request",
}
}
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalDetails {
pub operation: OperationType,
pub rp_id: String,
pub user_name: Option<String>,
pub credential_count: usize,
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UserPresence {
Ask,
Approved,
Denied,
}
#[cfg(all(unix, feature = "libusb"))]
struct PendingApproval {
id: ApprovalId,
details: ApprovalDetails,
connection_id: Option<String>,
response_tx: oneshot::Sender<ApprovalDecision>,
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone)]
pub struct ApprovalConfig {
pub auto_approve: bool,
pub timeout: Duration,
pub timeout_decision: ApprovalDecision,
}
#[cfg(all(unix, feature = "libusb"))]
impl Default for ApprovalConfig {
fn default() -> Self {
Self {
auto_approve: false,
timeout: Duration::from_secs(30),
timeout_decision: ApprovalDecision::Denied,
}
}
}
#[cfg(all(unix, feature = "libusb"))]
pub struct ApprovalManager {
config: Arc<RwLock<ApprovalConfig>>,
pending: Arc<Mutex<HashMap<ApprovalId, PendingApproval>>>,
next_id: Arc<AtomicU64>,
}
#[cfg(all(unix, feature = "libusb"))]
impl ApprovalManager {
pub fn new(config: ApprovalConfig) -> Self {
Self {
config: Arc::new(RwLock::new(config)),
pending: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicU64::new(1)),
}
}
pub fn auto_approve(&self) -> bool {
self.read_config().auto_approve
}
pub fn open(
&self,
details: ApprovalDetails,
connection_id: Option<String>,
) -> (ApprovalId, oneshot::Receiver<ApprovalDecision>) {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let (tx, rx) = oneshot::channel();
self.lock_pending().insert(
id,
PendingApproval {
id,
details: details.clone(),
connection_id,
response_tx: tx,
},
);
info!(
"FIDO2 approval {} opened: {:?} for RP '{}' (user: {:?})",
id, details.operation, details.rp_id, details.user_name
);
(id, rx)
}
pub async fn wait(
&self,
id: ApprovalId,
rx: oneshot::Receiver<ApprovalDecision>,
) -> ApprovalDecision {
let (auto_approve, timeout, timeout_decision) = {
let cfg = self.read_config();
(cfg.auto_approve, cfg.timeout, cfg.timeout_decision)
};
if auto_approve {
self.lock_pending().remove(&id);
info!("FIDO2 approval {} auto-approved (auto_approve=true)", id);
return ApprovalDecision::Approved;
}
let decision = match tokio::time::timeout(timeout, rx).await {
Ok(Ok(decision)) => {
info!("FIDO2 approval {} decided: {:?}", id, decision);
decision
}
Ok(Err(_)) => {
warn!(
"FIDO2 approval {} channel closed without a decision, denying",
id
);
timeout_decision
}
Err(_) => {
warn!(
"FIDO2 approval {} unanswered after {:?}, denying",
id, timeout
);
timeout_decision
}
};
self.lock_pending().remove(&id);
decision
}
pub async fn request_approval(
&self,
details: ApprovalDetails,
connection_id: Option<String>,
) -> (ApprovalId, ApprovalDecision) {
let (id, rx) = self.open(details, connection_id);
let decision = self.wait(id, rx).await;
(id, decision)
}
pub fn approve(&self, id: ApprovalId) -> Result<(), String> {
self.resolve(id, ApprovalDecision::Approved)
}
pub fn deny(&self, id: ApprovalId) -> Result<(), String> {
self.resolve(id, ApprovalDecision::Denied)
}
fn resolve(&self, id: ApprovalId, decision: ApprovalDecision) -> Result<(), String> {
let request = self.lock_pending().remove(&id);
match request {
Some(request) => {
info!(
"FIDO2 approval {} resolved {:?} ({:?} for RP '{}')",
id, decision, request.details.operation, request.details.rp_id
);
let _ = request.response_tx.send(decision);
Ok(())
}
None => {
let open = self.list_pending();
warn!("FIDO2 approval {} not found or already resolved", id);
if open.is_empty() {
Err(format!(
"no FIDO2 approval with id {} is pending (none are)",
id
))
} else {
Err(format!(
"no FIDO2 approval with id {} is pending; open ids are {:?}",
id,
open.iter().map(|p| p.id).collect::<Vec<_>>()
))
}
}
}
}
pub fn list_pending(&self) -> Vec<PendingApprovalInfo> {
let mut open: Vec<PendingApprovalInfo> = self
.lock_pending()
.values()
.map(|req| PendingApprovalInfo {
id: req.id,
details: req.details.clone(),
connection_id: req.connection_id.clone(),
})
.collect();
open.sort_by_key(|p| p.id);
open
}
pub fn set_config(&self, new_config: ApprovalConfig) {
*self
.config
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = new_config.clone();
info!("FIDO2 approval config updated: {:?}", new_config);
}
pub fn get_config(&self) -> ApprovalConfig {
self.read_config()
}
fn read_config(&self) -> ApprovalConfig {
self.config
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
fn lock_pending(&self) -> std::sync::MutexGuard<'_, HashMap<ApprovalId, PendingApproval>> {
self.pending
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone)]
pub struct PendingApprovalInfo {
pub id: ApprovalId,
pub details: ApprovalDetails,
pub connection_id: Option<String>,
}
#[cfg(all(unix, feature = "libusb"))]
impl Clone for ApprovalManager {
fn clone(&self) -> Self {
Self {
config: Arc::clone(&self.config),
pending: Arc::clone(&self.pending),
next_id: Arc::clone(&self.next_id),
}
}
}