use super::{Device, DeviceInfo, YUBICO_VENDOR_ID};
use anyhow::Result;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SerialProbe {
Is(u32),
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pairing {
Device(usize),
Touch(Vec<usize>),
NotFound,
}
pub fn decide(serial: u32, probes: &[SerialProbe], piv_yubikeys: usize) -> Pairing {
if let Some(i) = probes.iter().position(|p| *p == SerialProbe::Is(serial)) {
return Pairing::Device(i);
}
let unknown: Vec<usize> = probes
.iter()
.enumerate()
.filter(|(_, p)| **p == SerialProbe::Unknown)
.map(|(i, _)| i)
.collect();
match unknown.len() {
0 => Pairing::NotFound,
1 if probes.len() == 1 && piv_yubikeys == 1 => Pairing::Device(unknown[0]),
_ => Pairing::Touch(unknown),
}
}
pub fn yubikeys() -> Result<Vec<DeviceInfo>> {
Ok(super::list_devices()?
.into_iter()
.filter(|d| d.vendor_id == YUBICO_VENDOR_ID)
.collect())
}
pub fn probe_serial(info: &DeviceInfo) -> SerialProbe {
#[cfg(target_os = "linux")]
{
let read = super::hid::Hidraw::open(&info.path)
.and_then(|mut ch| super::hid::read_serial(&mut ch));
if let Ok(Some(serial)) = read {
return SerialProbe::Is(serial);
}
}
let _ = info;
SerialProbe::Unknown
}
pub fn touch_select(devices: &[Device], timeout: Duration) -> Result<usize> {
for dev in devices {
dev.touch_begin()?;
}
let start = Instant::now();
let result = loop {
let mut found = None;
for (i, dev) in devices.iter().enumerate() {
if dev.touched(50)? {
found = Some(i);
break;
}
}
if let Some(i) = found {
break Ok(i);
}
if start.elapsed() >= timeout {
break Err(super::Fido2Error::NoTouch.into());
}
};
for dev in devices {
dev.cancel();
}
result
}
pub fn open_yubikey(
serial: u32,
piv_yubikeys: usize,
on_touch_select: &dyn Fn(),
) -> Result<Device> {
#[cfg(all(unix, feature = "test-fixture"))]
if let Some(path) = super::socket::socket_path() {
return open_emulator(serial, &path);
}
let candidates = yubikeys()?;
if candidates.is_empty() {
return Err(super::Fido2Error::NoDevice.into());
}
let probes: Vec<SerialProbe> = candidates.iter().map(probe_serial).collect();
match decide(serial, &probes, piv_yubikeys) {
Pairing::Device(i) => Device::open(&candidates[i].path),
Pairing::NotFound => Err(super::Fido2Error::NoDevice.into()),
Pairing::Touch(indices) => {
let devices = indices
.iter()
.map(|&i| Device::open(&candidates[i].path))
.collect::<Result<Vec<_>>>()?;
on_touch_select();
let chosen = touch_select(&devices, Duration::from_secs(30))?;
Ok(devices.into_iter().nth(chosen).expect("chosen among them"))
}
}
}
pub fn open_without_touch(serial: u32, piv_yubikeys: usize) -> Result<Option<Device>> {
#[cfg(all(unix, feature = "test-fixture"))]
if let Some(path) = super::socket::socket_path() {
return open_emulator(serial, &path).map(Some);
}
let candidates = yubikeys()?;
if candidates.is_empty() {
return Err(super::Fido2Error::NoDevice.into());
}
let probes: Vec<SerialProbe> = candidates.iter().map(probe_serial).collect();
match decide(serial, &probes, piv_yubikeys) {
Pairing::Device(i) => Device::open(&candidates[i].path).map(Some),
Pairing::NotFound => Err(super::Fido2Error::NoDevice.into()),
Pairing::Touch(_) => Ok(None),
}
}
#[cfg(all(unix, feature = "test-fixture"))]
fn open_emulator(serial: u32, path: &str) -> Result<Device> {
let mut ch = super::socket::SocketChannel::open(path)?;
if super::hid::read_serial(&mut ch)? != Some(serial) {
return Err(super::Fido2Error::NoDevice.into());
}
drop(ch);
Device::open_with_io_path(path, &super::socket::SOCKET_IO)
}
#[cfg(test)]
mod tests {
use super::*;
use SerialProbe::{Is, Unknown};
#[test]
fn by_serial() {
assert_eq!(decide(7, &[Is(5), Is(7), Unknown], 3), Pairing::Device(1));
}
#[test]
fn a_different_serial_rules_out() {
assert_eq!(decide(7, &[Is(5)], 1), Pairing::NotFound);
assert_eq!(decide(7, &[Is(5), Unknown], 2), Pairing::Touch(vec![1]));
}
#[test]
fn one_yubikey_on_each_side() {
assert_eq!(decide(7, &[Unknown], 1), Pairing::Device(0));
assert_eq!(decide(7, &[Unknown], 2), Pairing::Touch(vec![0]));
}
#[test]
fn several_unknown() {
assert_eq!(
decide(7, &[Unknown, Unknown], 2),
Pairing::Touch(vec![0, 1])
);
}
}