diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/authenticator/src/transport | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator/src/transport')
55 files changed, 5339 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/src/transport/device_selector.rs b/third_party/rust/authenticator/src/transport/device_selector.rs new file mode 100644 index 0000000000..a0ce4ccb57 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/device_selector.rs @@ -0,0 +1,475 @@ +use crate::transport::hid::HIDDevice; +pub use crate::transport::platform::device::Device; +use runloop::RunLoop; +use std::collections::{HashMap, HashSet}; +use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; +use std::time::Duration; + +// This import is used, but Rust 1.68 gives a warning +#[allow(unused_imports)] +use crate::u2ftypes::U2FDevice; + +pub type DeviceID = <Device as HIDDevice>::Id; +pub type DeviceBuildParameters = <Device as HIDDevice>::BuildParameters; + +trait DeviceSelectorEventMarker {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlinkResult { + DeviceSelected, + Cancelled, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceCommand { + Blink, + Cancel, + Continue, + Removed, +} + +#[derive(Debug)] +pub enum DeviceSelectorEvent { + Cancel, + Timeout, + DevicesAdded(Vec<DeviceID>), + DeviceRemoved(DeviceID), + NotAToken(DeviceID), + ImAToken((DeviceID, Sender<DeviceCommand>)), + SelectedToken(DeviceID), +} + +pub struct DeviceSelector { + /// How to send a message to the event loop + sender: Sender<DeviceSelectorEvent>, + /// Thread of the event loop + runloop: RunLoop, +} + +impl DeviceSelector { + pub fn run() -> Self { + let (selector_send, selector_rec) = channel(); + // let new_device_callback = Arc::new(new_device_cb); + let runloop = RunLoop::new(move |alive| { + let mut blinking = false; + // Device was added, but we wait for its response, if it is a token or not + // We save both a write-only copy of the device (for cancellation) and it's thread + let mut waiting_for_response = HashSet::new(); + // Device IDs of devices that responded with "ImAToken" mapping to channels that are + // waiting to receive a DeviceCommand + let mut tokens = HashMap::new(); + while alive() { + let d = Duration::from_secs(100); + let res = match selector_rec.recv_timeout(d) { + Err(RecvTimeoutError::Disconnected) => { + break; + } + Err(RecvTimeoutError::Timeout) => DeviceSelectorEvent::Timeout, + Ok(res) => res, + }; + + match res { + DeviceSelectorEvent::Timeout | DeviceSelectorEvent::Cancel => { + /* TODO */ + Self::cancel_all(tokens, None); + break; + } + DeviceSelectorEvent::SelectedToken(ref id) => { + Self::cancel_all(tokens, Some(id)); + break; // We are done here. The selected device continues without us. + } + DeviceSelectorEvent::DevicesAdded(ids) => { + for id in ids { + debug!("Device added event: {:?}", id); + waiting_for_response.insert(id); + } + continue; + } + DeviceSelectorEvent::DeviceRemoved(ref id) => { + debug!("Device removed event: {:?}", id); + if !waiting_for_response.remove(id) { + // Note: We _could_ check here if we had multiple tokens and are already blinking + // and the removal of this one leads to only one token left. So we could in theory + // stop blinking and select it right away. At the moment, I think this is a + // too surprising behavior and therefore, we let the remaining device keep on blinking + // since the user could add yet another device, instead of using the remaining one. + tokens.iter().for_each(|(dev_id, tx)| { + if dev_id == id { + let _ = tx.send(DeviceCommand::Removed); + } + }); + tokens.retain(|dev_id, _| dev_id != id); + if tokens.is_empty() { + blinking = false; + continue; + } + } + // We are already blinking, so no need to run the code below this match + // that figures out if we should blink or not. In fact, currently, we do + // NOT want to run this code again, because if you have 2 blinking tokens + // and one got removed, we WANT the remaining one to continue blinking. + // This is a design choice, because I currently think it is the "less surprising" + // option to the user. + if blinking { + continue; + } + } + DeviceSelectorEvent::NotAToken(ref id) => { + debug!("Device not a token event: {:?}", id); + waiting_for_response.remove(id); + } + DeviceSelectorEvent::ImAToken((id, tx)) => { + let _ = waiting_for_response.remove(&id); + if blinking { + // We are already blinking, so this new device should blink too. + if tx.send(DeviceCommand::Blink).is_ok() { + tokens.insert(id, tx.clone()); + } + continue; + } else { + tokens.insert(id, tx.clone()); + } + } + } + + // All known devices told us, whether they are tokens or not and we have at least one token + if waiting_for_response.is_empty() && !tokens.is_empty() { + if tokens.len() == 1 { + let (dev_id, tx) = tokens.drain().next().unwrap(); // We just checked that it can't be empty + if tx.send(DeviceCommand::Continue).is_err() { + // Device thread died in the meantime (which shouldn't happen). + // Tokens is empty, so we just start over again + continue; + } + Self::cancel_all(tokens, Some(&dev_id)); + break; // We are done here + } else { + blinking = true; + + tokens.iter().for_each(|(_dev, tx)| { + // A send operation can only fail if the receiving end of a channel is disconnected, implying that the data could never be received. + // We ignore errors here for now, but should probably remove the device in such a case (even though it theoretically can't happen) + let _ = tx.send(DeviceCommand::Blink); + }); + } + } + } + }); + Self { + runloop: runloop.unwrap(), // TODO + sender: selector_send, + } + } + + pub fn clone_sender(&self) -> Sender<DeviceSelectorEvent> { + self.sender.clone() + } + + fn cancel_all(tokens: HashMap<DeviceID, Sender<DeviceCommand>>, exclude: Option<&DeviceID>) { + for (dev_id, tx) in tokens.iter() { + if Some(dev_id) != exclude { + let _ = tx.send(DeviceCommand::Cancel); + } + } + } + + pub fn stop(&mut self) { + // We ignore a possible error here, since we don't really care + let _ = self.sender.send(DeviceSelectorEvent::Cancel); + self.runloop.cancel(); + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{ + consts::Capability, + ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorOptions}, + u2ftypes::U2FDeviceInfo, + }; + + fn gen_info(id: String) -> U2FDeviceInfo { + U2FDeviceInfo { + vendor_name: String::from("ExampleVendor").into_bytes(), + device_name: id.into_bytes(), + version_interface: 1, + version_major: 3, + version_minor: 2, + version_build: 1, + cap_flags: Capability::WINK | Capability::CBOR | Capability::NMSG, + } + } + + fn make_device_simple_u2f(dev: &mut Device) { + dev.set_device_info(gen_info(dev.id())); + dev.create_channel(); + } + + fn make_device_with_pin(dev: &mut Device) { + dev.set_device_info(gen_info(dev.id())); + dev.create_channel(); + let info = AuthenticatorInfo { + options: AuthenticatorOptions { + client_pin: Some(true), + ..Default::default() + }, + ..Default::default() + }; + dev.set_authenticator_info(info); + } + + fn send_i_am_token(dev: &Device, selector: &DeviceSelector) { + selector + .sender + .send(DeviceSelectorEvent::ImAToken(( + dev.id(), + dev.sender.clone().unwrap(), + ))) + .unwrap(); + } + + fn send_no_token(dev: &Device, selector: &DeviceSelector) { + selector + .sender + .send(DeviceSelectorEvent::NotAToken(dev.id())) + .unwrap() + } + + fn remove_device(dev: &Device, selector: &DeviceSelector) { + selector + .sender + .send(DeviceSelectorEvent::DeviceRemoved(dev.id())) + .unwrap(); + assert_eq!( + dev.receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Removed + ); + } + + fn add_devices<'a, T>(iter: T, selector: &DeviceSelector) + where + T: Iterator<Item = &'a Device>, + { + selector + .sender + .send(DeviceSelectorEvent::DevicesAdded( + iter.map(|f| f.id()).collect(), + )) + .unwrap(); + } + + #[test] + fn test_device_selector_one_token_no_late_adds() { + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_with_pin(&mut devices[2]); + let selector = DeviceSelector::run(); + + // Adding all + add_devices(devices.iter(), &selector); + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(d, &selector); + } + }); + + send_i_am_token(&devices[2], &selector); + + assert_eq!( + devices[2].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Continue + ); + } + + // This test is mostly for testing stop() and clone_sender() + #[test] + fn test_device_selector_stop() { + let device = Device::new("device selector 1").unwrap(); + + let mut selector = DeviceSelector::run(); + + // Adding all + selector + .clone_sender() + .send(DeviceSelectorEvent::DevicesAdded(vec![device.id()])) + .unwrap(); + + selector + .clone_sender() + .send(DeviceSelectorEvent::NotAToken(device.id())) + .unwrap(); + selector.stop(); + } + + #[test] + fn test_device_selector_all_pins_with_late_add() { + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + Device::new("device selector 5").unwrap(), + Device::new("device selector 6").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_with_pin(&mut devices[2]); + make_device_with_pin(&mut devices[4]); + make_device_with_pin(&mut devices[5]); + + let selector = DeviceSelector::run(); + + // Adding all, except the last one (we simulate that this one is not yet plugged in) + add_devices(devices.iter().take(5), &selector); + + // Interleave tokens and non-tokens + send_i_am_token(&devices[2], &selector); + + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(d, &selector); + } + }); + + send_i_am_token(&devices[4], &selector); + + // We added 2 devices that are tokens. They should get the blink-command now + assert_eq!( + devices[2].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + assert_eq!( + devices[4].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + + // Plug in late device + send_i_am_token(&devices[5], &selector); + assert_eq!( + devices[5].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + } + + #[test] + fn test_device_selector_no_pins_late_mixed_adds() { + // Multiple tokes, none of them support a PIN + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + Device::new("device selector 5").unwrap(), + Device::new("device selector 6").unwrap(), + Device::new("device selector 7").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_simple_u2f(&mut devices[2]); + make_device_simple_u2f(&mut devices[4]); + make_device_simple_u2f(&mut devices[5]); + + let selector = DeviceSelector::run(); + + // Adding all, except the last one (we simulate that this one is not yet plugged in) + add_devices(devices.iter().take(5), &selector); + + // Interleave tokens and non-tokens + send_i_am_token(&devices[2], &selector); + + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(d, &selector); + } + }); + + send_i_am_token(&devices[4], &selector); + + // We added 2 devices that are tokens. They should get the blink-command now + assert_eq!( + devices[2].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + assert_eq!( + devices[4].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + + // Plug in late device + send_i_am_token(&devices[5], &selector); + assert_eq!( + devices[5].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + // Remove device again + remove_device(&devices[5], &selector); + + // Now we add a token that has a PIN, it should not get "Continue" but "Blink" + make_device_with_pin(&mut devices[6]); + send_i_am_token(&devices[6], &selector); + assert_eq!( + devices[6].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + } + + #[test] + fn test_device_selector_mixed_pins_remove_all() { + // Multiple tokes, none of them support a PIN, so we should get Continue-commands + // for all of them + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + Device::new("device selector 5").unwrap(), + Device::new("device selector 6").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_with_pin(&mut devices[2]); + make_device_with_pin(&mut devices[4]); + make_device_with_pin(&mut devices[5]); + + let selector = DeviceSelector::run(); + + // Adding all, except the last one (we simulate that this one is not yet plugged in) + add_devices(devices.iter(), &selector); + + devices.iter_mut().for_each(|d| { + if d.is_u2f() { + send_i_am_token(d, &selector); + } else { + send_no_token(d, &selector); + } + }); + + for idx in [2, 4, 5] { + assert_eq!( + devices[idx].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + } + + // Remove all tokens + for idx in [2, 4, 5] { + remove_device(&devices[idx], &selector); + } + + // Adding one again + send_i_am_token(&devices[4], &selector); + + // This should now get a "Continue" instead of "Blinking", because it's the only device + assert_eq!( + devices[4].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Continue + ); + } +} diff --git a/third_party/rust/authenticator/src/transport/errors.rs b/third_party/rust/authenticator/src/transport/errors.rs new file mode 100644 index 0000000000..451c27d6e8 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/errors.rs @@ -0,0 +1,98 @@ +use crate::consts::{SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, SW_WRONG_DATA, SW_WRONG_LENGTH}; +use crate::ctap2::commands::CommandError; +use std::fmt; +use std::io; +use std::path; + +#[allow(unused)] +#[derive(Debug, PartialEq, Eq)] +pub enum ApduErrorStatus { + ConditionsNotSatisfied, + WrongData, + WrongLength, + Unknown([u8; 2]), +} + +impl ApduErrorStatus { + pub fn from(status: [u8; 2]) -> Result<(), ApduErrorStatus> { + match status { + s if s == SW_NO_ERROR => Ok(()), + s if s == SW_CONDITIONS_NOT_SATISFIED => Err(ApduErrorStatus::ConditionsNotSatisfied), + s if s == SW_WRONG_DATA => Err(ApduErrorStatus::WrongData), + s if s == SW_WRONG_LENGTH => Err(ApduErrorStatus::WrongLength), + other => Err(ApduErrorStatus::Unknown(other)), + } + } +} + +impl fmt::Display for ApduErrorStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ApduErrorStatus::ConditionsNotSatisfied => write!(f, "Apdu: condition not satisfied"), + ApduErrorStatus::WrongData => write!(f, "Apdu: wrong data"), + ApduErrorStatus::WrongLength => write!(f, "Apdu: wrong length"), + ApduErrorStatus::Unknown(ref u) => write!(f, "Apdu: unknown error: {u:?}"), + } + } +} + +#[allow(unused)] +#[derive(Debug)] +pub enum HIDError { + /// Transport replied with a status not expected + DeviceError, + UnexpectedInitReplyLen, + NonceMismatch, + DeviceNotInitialized, + DeviceNotSupported, + UnsupportedCommand, + UnexpectedVersion, + IO(Option<path::PathBuf>, io::Error), + UnexpectedCmd(u8), + Command(CommandError), + ApduStatus(ApduErrorStatus), +} + +impl From<io::Error> for HIDError { + fn from(e: io::Error) -> HIDError { + HIDError::IO(None, e) + } +} + +impl From<CommandError> for HIDError { + fn from(e: CommandError) -> HIDError { + HIDError::Command(e) + } +} + +impl From<ApduErrorStatus> for HIDError { + fn from(e: ApduErrorStatus) -> HIDError { + HIDError::ApduStatus(e) + } +} + +impl fmt::Display for HIDError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + HIDError::UnexpectedInitReplyLen => { + write!(f, "Error: Unexpected reply len when initilizaling") + } + HIDError::NonceMismatch => write!(f, "Error: Nonce mismatch"), + HIDError::DeviceError => write!(f, "Error: device returned error"), + HIDError::DeviceNotInitialized => write!(f, "Error: using not initiliazed device"), + HIDError::DeviceNotSupported => { + write!(f, "Error: requested operation is not available on device") + } + HIDError::UnexpectedVersion => write!(f, "Error: Unexpected protocol version"), + HIDError::UnsupportedCommand => { + write!(f, "Error: command is not supported on this device") + } + HIDError::IO(ref p, ref e) => write!(f, "Error: Ioerror({p:?}): {e}"), + HIDError::Command(ref e) => write!(f, "Error: Error issuing command: {e}"), + HIDError::UnexpectedCmd(s) => write!(f, "Error: Unexpected status: {s}"), + HIDError::ApduStatus(ref status) => { + write!(f, "Error: Unexpected apdu status: {status:?}") + } + } + } +} diff --git a/third_party/rust/authenticator/src/transport/freebsd/device.rs b/third_party/rust/authenticator/src/transport/freebsd/device.rs new file mode 100644 index 0000000000..7a350c067e --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/device.rs @@ -0,0 +1,217 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::uhid; +use crate::transport::{FidoDevice, HIDError, SharedSecret}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::from_unix_result; +use crate::util::io_err; +use std::ffi::{CString, OsString}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::mem; +use std::os::unix::prelude::*; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: libc::c_int, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + fn ping(&mut self) -> io::Result<()> { + for i in 0..10 { + let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE]; + + buf[0] = 0; // report number + buf[1] = 0xff; // CID_BROADCAST + buf[2] = 0xff; + buf[3] = 0xff; + buf[4] = 0xff; + buf[5] = 0x81; // ping + buf[6] = 0; + buf[7] = 1; // one byte + + if self.write(&buf)? != buf.len() { + return Err(io_err("write ping failed")); + } + + // Wait for response + let mut pfd: libc::pollfd = unsafe { mem::zeroed() }; + pfd.fd = self.fd; + pfd.events = libc::POLLIN; + let nfds = unsafe { libc::poll(&mut pfd, 1, 100) }; + if nfds == -1 { + return Err(io::Error::last_os_error()); + } + if nfds == 0 { + debug!("device timeout {}", i); + continue; + } + + // Read response. When reports come in they are all + // exactly the same size, with no report id byte because + // there is only one report. + let n = self.read(&mut buf[1..])?; + if n != buf.len() - 1 { + return Err(io_err("read pong failed")); + } + + return Ok(()); + } + + Err(io_err("no response from device")) + } +} + +impl Drop for Device { + fn drop(&mut self) { + // Close the fd, ignore any errors. + let _ = unsafe { libc::close(self.fd) }; + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let bufp = buf.as_mut_ptr() as *mut libc::c_void; + let rv = unsafe { libc::read(self.fd, bufp, buf.len()) }; + from_unix_result(rv as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + let report_id = buf[0] as i64; + // Skip report number when not using numbered reports. + let start = if report_id == 0x0 { 1 } else { 0 }; + let data = &buf[start..]; + + let data_ptr = data.as_ptr() as *const libc::c_void; + let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) }; + from_unix_result(rv as usize + 1) + } + + // USB HID writes don't buffer, so this will be a nop. + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = OsString; + type Id = OsString; + + fn new(path: OsString) -> Result<Self, (HIDError, Self::Id)> { + let cstr = + CString::new(path.as_bytes()).map_err(|_| (HIDError::DeviceError, path.clone()))?; + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) }; + let fd = from_unix_result(fd).map_err(|e| (e.into(), path.clone()))?; + let mut res = Self { + path, + fd, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + if !uhid::is_u2f_device(self.fd) { + return false; + } + if self.ping().is_err() { + return false; + } + true + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/freebsd/mod.rs b/third_party/rust/authenticator/src/transport/freebsd/mod.rs new file mode 100644 index 0000000000..7ed5727157 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod monitor; +mod uhid; diff --git a/third_party/rust/authenticator/src/transport/freebsd/monitor.rs b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs new file mode 100644 index 0000000000..340ebef836 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use devd_rs; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::ffi::OsString; +use std::sync::{mpsc::Sender, Arc}; +use std::{fs, io}; + +const POLL_TIMEOUT: usize = 100; + +pub enum Event { + Add(OsString), + Remove(OsString), +} + +impl Event { + fn from_devd(event: devd_rs::Event) -> Option<Self> { + match event { + devd_rs::Event::Attach { + ref dev, + parent: _, + location: _, + } if dev.starts_with("uhid") => Some(Event::Add(("/dev/".to_owned() + dev).into())), + devd_rs::Event::Detach { + ref dev, + parent: _, + location: _, + } if dev.starts_with("uhid") => Some(Event::Remove(("/dev/".to_owned() + dev).into())), + _ => None, + } + } +} + +fn convert_error(e: devd_rs::Error) -> io::Error { + e.into() +} + +pub struct Monitor<F> +where + F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + let mut ctx = devd_rs::Context::new().map_err(convert_error)?; + + let mut initial_devs = Vec::new(); + // Iterate all existing devices. + for dev in (fs::read_dir("/dev")?).flatten() { + let filename_ = dev.file_name(); + let filename = filename_.to_str().unwrap_or(""); + if filename.starts_with("uhid") { + let path = OsString::from("/dev/".to_owned() + filename); + initial_devs.push(path.clone()); + self.add_device(path); + } + } + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(initial_devs)); + + // Loop until we're stopped by the controlling thread, or fail. + while alive() { + // Wait for new events, break on failure. + match ctx.wait_for_event(POLL_TIMEOUT) { + Err(devd_rs::Error::Timeout) => (), + Err(e) => return Err(convert_error(e).into()), + Ok(event) => { + if let Some(event) = Event::from_devd(event) { + self.process_event(event); + } + } + } + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn process_event(&mut self, event: Event) { + match event { + Event::Add(path) => { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()])); + self.add_device(path); + } + Event::Remove(path) => { + self.remove_device(path); + } + } + } + + fn add_device(&mut self, path: OsString) { + let f = self.new_device_cb.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + let key = path.clone(); + debug!("Adding device {}", key.to_string_lossy()); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: OsString) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path.to_string_lossy()); + if let Some(runloop) = self.runloops.remove(&path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/freebsd/transaction.rs b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs new file mode 100644 index 0000000000..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/freebsd/uhid.rs b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs new file mode 100644 index 0000000000..681b09a768 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use std::io; +use std::os::unix::io::RawFd; +use std::ptr; + +use crate::transport::hidproto::*; +use crate::util::from_unix_result; + +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug)] +pub struct GenDescriptor { + ugd_data: *mut u8, + ugd_lang_id: u16, + ugd_maxlen: u16, + ugd_actlen: u16, + ugd_offset: u16, + ugd_config_index: u8, + ugd_string_index: u8, + ugd_iface_index: u8, + ugd_altif_index: u8, + ugd_endpt_index: u8, + ugd_report_index: u8, + reserved: [u8; 16], +} + +impl Default for GenDescriptor { + fn default() -> GenDescriptor { + GenDescriptor { + ugd_data: ptr::null_mut(), + ugd_lang_id: 0, + ugd_maxlen: 65535, + ugd_actlen: 0, + ugd_offset: 0, + ugd_config_index: 0, + ugd_string_index: 0, + ugd_iface_index: 0, + ugd_altif_index: 0, + ugd_endpt_index: 0, + ugd_report_index: 0, + reserved: [0; 16], + } + } +} + +const IOWR: u32 = 0x40000000 | 0x80000000; + +const IOCPARM_SHIFT: u32 = 13; +const IOCPARM_MASK: u32 = (1 << IOCPARM_SHIFT) - 1; + +const TYPESHIFT: u32 = 8; +const SIZESHIFT: u32 = 16; + +macro_rules! ioctl { + ($dir:expr, $name:ident, $ioty:expr, $nr:expr, $size:expr; $ty:ty) => { + pub unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> { + let ioc = ($dir as u32) + | (($size as u32 & IOCPARM_MASK) << SIZESHIFT) + | (($ioty as u32) << TYPESHIFT) + | ($nr as u32); + from_unix_result(libc::ioctl(fd, ioc as libc::c_ulong, val)) + } + }; +} + +// https://github.com/freebsd/freebsd/blob/master/sys/dev/usb/usb_ioctl.h +ioctl!(IOWR, usb_get_report_desc, b'U', 21, 32; /*struct*/ GenDescriptor); + +fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> { + let mut desc = GenDescriptor::default(); + let _ = unsafe { usb_get_report_desc(fd, &mut desc)? }; + desc.ugd_maxlen = desc.ugd_actlen; + let mut value = vec![0; desc.ugd_actlen as usize]; + desc.ugd_data = value.as_mut_ptr(); + let _ = unsafe { usb_get_report_desc(fd, &mut desc)? }; + Ok(ReportDescriptor { value }) +} + +pub fn is_u2f_device(fd: RawFd) -> bool { + match read_report_descriptor(fd) { + Ok(desc) => has_fido_usage(desc), + Err(_) => false, // Upon failure, just say it's not a U2F device. + } +} diff --git a/third_party/rust/authenticator/src/transport/hid.rs b/third_party/rust/authenticator/src/transport/hid.rs new file mode 100644 index 0000000000..a789344754 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/hid.rs @@ -0,0 +1,153 @@ +use crate::consts::{HIDCmd, CID_BROADCAST}; +use crate::crypto::SharedSecret; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::{errors::HIDError, Nonce}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp}; +use rand::{thread_rng, RngCore}; +use std::cmp::Eq; +use std::fmt; +use std::hash::Hash; +use std::io; + +pub trait HIDDevice +where + Self: io::Read, + Self: io::Write, + Self: U2FDevice, + Self: Sized, + Self: fmt::Debug, +{ + type BuildParameters: Sized; + type Id: fmt::Debug + PartialEq + Eq + Hash + Sized; + + // Open device, verify that it is indeed a CTAP device and potentially read initial values + fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)>; + fn id(&self) -> Self::Id; + fn initialized(&self) -> bool; + // Check if the device is actually a token + fn is_u2f(&mut self) -> bool; + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>; + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo); + fn set_shared_secret(&mut self, secret: SharedSecret); + fn get_shared_secret(&self) -> Option<&SharedSecret>; + + // Initialize on a protocol-level + fn initialize(&mut self, noncecmd: Nonce) -> Result<(), HIDError> { + if self.initialized() { + return Ok(()); + } + + let nonce = match noncecmd { + Nonce::Use(x) => x, + Nonce::CreateRandom => { + let mut nonce = [0u8; 8]; + thread_rng().fill_bytes(&mut nonce); + nonce + } + }; + + // Send Init to broadcast address to create a new channel + self.set_cid(CID_BROADCAST); + let (cmd, raw) = self.sendrecv(HIDCmd::Init, &nonce, &|| true)?; + if cmd != HIDCmd::Init { + return Err(HIDError::DeviceError); + } + + let rsp = U2FHIDInitResp::read(&raw, &nonce)?; + // Get the new Channel ID + self.set_cid(rsp.cid); + + let vendor = self + .get_property("Manufacturer") + .unwrap_or_else(|_| String::from("Unknown Vendor")); + let product = self + .get_property("Product") + .unwrap_or_else(|_| String::from("Unknown Device")); + + let info = U2FDeviceInfo { + vendor_name: vendor.as_bytes().to_vec(), + device_name: product.as_bytes().to_vec(), + version_interface: rsp.version_interface, + version_major: rsp.version_major, + version_minor: rsp.version_minor, + version_build: rsp.version_build, + cap_flags: rsp.cap_flags, + }; + debug!("{:?}: {:?}", self.id(), info); + self.set_device_info(info); + + // A CTAPHID host SHALL accept a response size that is longer than the + // anticipated size to allow for future extensions of the protocol, yet + // maintaining backwards compatibility. Future versions will maintain + // the response structure of the current version, but additional fields + // may be added. + + Ok(()) + } + + fn sendrecv( + &mut self, + cmd: HIDCmd, + send: &[u8], + keep_alive: &dyn Fn() -> bool, + ) -> io::Result<(HIDCmd, Vec<u8>)> { + let cmd: u8 = cmd.into(); + self.u2f_write(cmd, send)?; + loop { + let (cmd, data) = self.u2f_read()?; + if cmd != HIDCmd::Keepalive { + return Ok((cmd, data)); + } + // The authenticator might send us HIDCmd::Keepalive messages indefinitely, e.g. if + // it's waiting for user presence. The keep_alive function is used to cancel the + // transaction. + if !keep_alive() { + break; + } + } + + // If this is a CTAP2 device we can tell the authenticator to cancel the transaction on its + // side as well. There's nothing to do for U2F/CTAP1 devices. + if self.get_authenticator_info().is_some() { + self.u2f_write(u8::from(HIDCmd::Cancel), &[])?; + } + // For CTAP2 devices we expect to read + // (HIDCmd::Cbor, [CTAP2_ERR_KEEPALIVE_CANCEL]) + // for U2F/CTAP1 we expect to read + // (HIDCmd::Keepalive, [status]). + self.u2f_read() + } + + fn u2f_write(&mut self, cmd: u8, send: &[u8]) -> io::Result<()> { + let mut count = U2FHIDInit::write(self, cmd, send)?; + + // Send continuation packets. + let mut sequence = 0u8; + while count < send.len() { + count += U2FHIDCont::write(self, sequence, &send[count..])?; + sequence += 1; + } + + Ok(()) + } + + fn u2f_read(&mut self) -> io::Result<(HIDCmd, Vec<u8>)> { + // Now we read. This happens in 2 chunks: The initial packet, which has + // the size we expect overall, then continuation packets, which will + // fill in data until we have everything. + let (cmd, data) = { + let (cmd, mut data) = U2FHIDInit::read(self)?; + + trace!("init frame data read: {:04X?}", &data); + let mut sequence = 0u8; + while data.len() < data.capacity() { + let max = data.capacity() - data.len(); + data.extend_from_slice(&U2FHIDCont::read(self, sequence, max)?); + sequence += 1; + } + (cmd, data) + }; + trace!("u2f_read({:?}) cmd={:?}: {:04X?}", self.id(), cmd, &data); + Ok((cmd, data)) + } +} diff --git a/third_party/rust/authenticator/src/transport/hidproto.rs b/third_party/rust/authenticator/src/transport/hidproto.rs new file mode 100644 index 0000000000..2438eb730d --- /dev/null +++ b/third_party/rust/authenticator/src/transport/hidproto.rs @@ -0,0 +1,257 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Shared code for platforms that use raw HID access (Linux, FreeBSD, etc.) + +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::cast_lossless, clippy::needless_lifetimes) +)] + +#[cfg(any(target_os = "linux"))] +use std::io; +use std::mem; + +use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID}; +#[cfg(any(target_os = "linux"))] +use crate::consts::{INIT_HEADER_SIZE, MAX_HID_RPT_SIZE}; + +// The 4 MSBs (the tag) are set when it's a long item. +const HID_MASK_LONG_ITEM_TAG: u8 = 0b1111_0000; +// The 2 LSBs denote the size of a short item. +const HID_MASK_SHORT_ITEM_SIZE: u8 = 0b0000_0011; +// The 6 MSBs denote the tag (4) and type (2). +const HID_MASK_ITEM_TAGTYPE: u8 = 0b1111_1100; +// tag=0000, type=10 (local) +const HID_ITEM_TAGTYPE_USAGE: u8 = 0b0000_1000; +// tag=0000, type=01 (global) +const HID_ITEM_TAGTYPE_USAGE_PAGE: u8 = 0b0000_0100; +// tag=1000, type=00 (main) +const HID_ITEM_TAGTYPE_INPUT: u8 = 0b1000_0000; +// tag=1001, type=00 (main) +const HID_ITEM_TAGTYPE_OUTPUT: u8 = 0b1001_0000; +// tag=1001, type=01 (global) +const HID_ITEM_TAGTYPE_REPORT_COUNT: u8 = 0b1001_0100; + +pub struct ReportDescriptor { + pub value: Vec<u8>, +} + +impl ReportDescriptor { + fn iter(self) -> ReportDescriptorIterator { + ReportDescriptorIterator::new(self) + } +} + +#[derive(Debug)] +pub enum Data { + UsagePage { data: u32 }, + Usage { data: u32 }, + Input, + Output, + ReportCount { data: u32 }, +} + +pub struct ReportDescriptorIterator { + desc: ReportDescriptor, + pos: usize, +} + +impl ReportDescriptorIterator { + fn new(desc: ReportDescriptor) -> Self { + Self { desc, pos: 0 } + } + + fn next_item(&mut self) -> Option<Data> { + let item = get_hid_item(&self.desc.value[self.pos..]); + if item.is_none() { + self.pos = self.desc.value.len(); // Close, invalid data. + return None; + } + + let (tag_type, key_len, data) = item.unwrap(); + + // Advance if we have a valid item. + self.pos += key_len + data.len(); + + // We only check short items. + if key_len > 1 { + return None; // Check next item. + } + + // Short items have max. length of 4 bytes. + assert!(data.len() <= mem::size_of::<u32>()); + + // Convert data bytes to a uint. + let data = read_uint_le(data); + match tag_type { + HID_ITEM_TAGTYPE_USAGE_PAGE => Some(Data::UsagePage { data }), + HID_ITEM_TAGTYPE_USAGE => Some(Data::Usage { data }), + HID_ITEM_TAGTYPE_INPUT => Some(Data::Input), + HID_ITEM_TAGTYPE_OUTPUT => Some(Data::Output), + HID_ITEM_TAGTYPE_REPORT_COUNT => Some(Data::ReportCount { data }), + _ => None, + } + } +} + +impl Iterator for ReportDescriptorIterator { + type Item = Data; + + fn next(&mut self) -> Option<Self::Item> { + if self.pos >= self.desc.value.len() { + return None; + } + + self.next_item().or_else(|| self.next()) + } +} + +fn get_hid_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> { + if (buf[0] & HID_MASK_LONG_ITEM_TAG) == HID_MASK_LONG_ITEM_TAG { + get_hid_long_item(buf) + } else { + get_hid_short_item(buf) + } +} + +fn get_hid_long_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> { + // A valid long item has at least three bytes. + if buf.len() < 3 { + return None; + } + + let len = buf[1] as usize; + + // Ensure that there are enough bytes left in the buffer. + if len > buf.len() - 3 { + return None; + } + + Some((buf[2], 3 /* key length */, &buf[3..])) +} + +fn get_hid_short_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> { + // This is a short item. The bottom two bits of the key + // contain the length of the data section (value) for this key. + let len = match buf[0] & HID_MASK_SHORT_ITEM_SIZE { + s @ 0..=2 => s as usize, + _ => 4, /* _ == 3 */ + }; + + // Ensure that there are enough bytes left in the buffer. + if len > buf.len() - 1 { + return None; + } + + Some(( + buf[0] & HID_MASK_ITEM_TAGTYPE, + 1, /* key length */ + &buf[1..=len], + )) +} + +fn read_uint_le(buf: &[u8]) -> u32 { + assert!(buf.len() <= 4); + // Parse the number in little endian byte order. + buf.iter() + .rev() + .fold(0, |num, b| (num << 8) | (u32::from(*b))) +} + +pub fn has_fido_usage(desc: ReportDescriptor) -> bool { + let mut usage_page = None; + let mut usage = None; + + for data in desc.iter() { + match data { + Data::UsagePage { data } => usage_page = Some(data), + Data::Usage { data } => usage = Some(data), + _ => {} + } + + // Check the values we found. + if let (Some(usage_page), Some(usage)) = (usage_page, usage) { + return usage_page == u32::from(FIDO_USAGE_PAGE) + && usage == u32::from(FIDO_USAGE_U2FHID); + } + } + + false +} + +#[cfg(any(target_os = "linux"))] +pub fn read_hid_rpt_sizes(desc: ReportDescriptor) -> io::Result<(usize, usize)> { + let mut in_rpt_count = None; + let mut out_rpt_count = None; + let mut last_rpt_count = None; + + for data in desc.iter() { + match data { + Data::ReportCount { data } => { + if last_rpt_count.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Duplicate HID_ReportCount", + )); + } + last_rpt_count = Some(data as usize); + } + Data::Input => { + if last_rpt_count.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "HID_Input should be preceded by HID_ReportCount", + )); + } + if in_rpt_count.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Duplicate HID_ReportCount", + )); + } + in_rpt_count = last_rpt_count; + last_rpt_count = None + } + Data::Output => { + if last_rpt_count.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "HID_Output should be preceded by HID_ReportCount", + )); + } + if out_rpt_count.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Duplicate HID_ReportCount", + )); + } + out_rpt_count = last_rpt_count; + last_rpt_count = None; + } + _ => {} + } + } + + match (in_rpt_count, out_rpt_count) { + (Some(in_count), Some(out_count)) => { + if in_count > INIT_HEADER_SIZE + && in_count <= MAX_HID_RPT_SIZE + && out_count > INIT_HEADER_SIZE + && out_count <= MAX_HID_RPT_SIZE + { + Ok((in_count, out_count)) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Report size is too small or too large", + )) + } + } + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to extract report sizes from report descriptor", + )), + } +} diff --git a/third_party/rust/authenticator/src/transport/linux/device.rs b/third_party/rust/authenticator/src/transport/linux/device.rs new file mode 100644 index 0000000000..bad487c53c --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/device.rs @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +use crate::consts::CID_BROADCAST; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::{hidraw, monitor}; +use crate::transport::{FidoDevice, HIDError, SharedSecret}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::from_unix_result; +use std::fs::OpenOptions; +use std::hash::{Hash, Hasher}; +use std::io; +use std::io::{Read, Write}; +use std::os::unix::io::AsRawFd; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct Device { + path: PathBuf, + fd: std::fs::File, + in_rpt_size: usize, + out_rpt_size: usize, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let bufp = buf.as_mut_ptr() as *mut libc::c_void; + let rv = unsafe { libc::read(self.fd.as_raw_fd(), bufp, buf.len()) }; + from_unix_result(rv as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + let bufp = buf.as_ptr() as *const libc::c_void; + let rv = unsafe { libc::write(self.fd.as_raw_fd(), bufp, buf.len()) }; + from_unix_result(rv as usize) + } + + // USB HID writes don't buffer, so this will be a nop. + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + self.in_rpt_size + } + + fn out_rpt_size(&self) -> usize { + self.out_rpt_size + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + monitor::get_property_linux(&self.path, prop_name) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = PathBuf; + type Id = PathBuf; + + fn new(path: PathBuf) -> Result<Self, (HIDError, Self::Id)> { + debug!("Opening device {:?}", path); + let fd = OpenOptions::new() + .read(true) + .write(true) + .open(&path) + .map_err(|e| (HIDError::IO(Some(path.clone()), e), path.clone()))?; + let (in_rpt_size, out_rpt_size) = hidraw::read_hid_rpt_sizes_or_defaults(fd.as_raw_fd()); + let mut res = Self { + path, + fd, + in_rpt_size, + out_rpt_size, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path)) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + hidraw::is_u2f_device(self.fd.as_raw_fd()) + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/linux/hidraw.rs b/third_party/rust/authenticator/src/transport/linux/hidraw.rs new file mode 100644 index 0000000000..16d687f358 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/hidraw.rs @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))] + +extern crate libc; + +use std::io; +use std::os::unix::io::RawFd; + +use super::hidwrapper::{_HIDIOCGRDESC, _HIDIOCGRDESCSIZE}; +use crate::consts::MAX_HID_RPT_SIZE; +use crate::transport::hidproto::*; +use crate::util::{from_unix_result, io_err}; + +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct LinuxReportDescriptor { + size: ::libc::c_int, + value: [u8; 4096], +} + +const HID_MAX_DESCRIPTOR_SIZE: usize = 4096; + +#[cfg(not(target_env = "musl"))] +type IocType = libc::c_ulong; +#[cfg(target_env = "musl")] +type IocType = libc::c_int; + +pub unsafe fn hidiocgrdescsize( + fd: libc::c_int, + val: *mut ::libc::c_int, +) -> io::Result<libc::c_int> { + from_unix_result(libc::ioctl(fd, _HIDIOCGRDESCSIZE as IocType, val)) +} + +pub unsafe fn hidiocgrdesc( + fd: libc::c_int, + val: *mut LinuxReportDescriptor, +) -> io::Result<libc::c_int> { + from_unix_result(libc::ioctl(fd, _HIDIOCGRDESC as IocType, val)) +} + +pub fn is_u2f_device(fd: RawFd) -> bool { + match read_report_descriptor(fd) { + Ok(desc) => has_fido_usage(desc), + Err(_) => false, // Upon failure, just say it's not a U2F device. + } +} + +pub fn read_hid_rpt_sizes_or_defaults(fd: RawFd) -> (usize, usize) { + let default_rpt_sizes = (MAX_HID_RPT_SIZE, MAX_HID_RPT_SIZE); + let desc = read_report_descriptor(fd); + if let Ok(desc) = desc { + if let Ok(rpt_sizes) = read_hid_rpt_sizes(desc) { + rpt_sizes + } else { + default_rpt_sizes + } + } else { + default_rpt_sizes + } +} + +fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> { + let mut desc = LinuxReportDescriptor { + size: 0, + value: [0; HID_MAX_DESCRIPTOR_SIZE], + }; + + let _ = unsafe { hidiocgrdescsize(fd, &mut desc.size)? }; + if desc.size == 0 || desc.size as usize > desc.value.len() { + return Err(io_err("unexpected hidiocgrdescsize() result")); + } + + let _ = unsafe { hidiocgrdesc(fd, &mut desc)? }; + let mut value = Vec::from(&desc.value[..]); + value.truncate(desc.size as usize); + Ok(ReportDescriptor { value }) +} diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.h b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h new file mode 100644 index 0000000000..ce77e0f1ca --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h @@ -0,0 +1,12 @@ +#include<sys/ioctl.h> +#include<linux/hidraw.h> + +/* we define these constants to work around the fact that bindgen + can't deal with the _IOR macro function. We let cpp deal with it + for us. */ + +const __u32 _HIDIOCGRDESCSIZE = HIDIOCGRDESCSIZE; +#undef HIDIOCGRDESCSIZE + +const __u32 _HIDIOCGRDESC = HIDIOCGRDESC; +#undef HIDIOCGRDESC diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs new file mode 100644 index 0000000000..82aabc6301 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs @@ -0,0 +1,51 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// sadly we need this file so we can avoid the suprious warnings that +// would come with bindgen, as well as to avoid cluttering the mod.rs +// with spurious architecture specific modules. + +#[cfg(target_arch = "x86")] +include!("ioctl_x86.rs"); + +#[cfg(target_arch = "x86_64")] +include!("ioctl_x86_64.rs"); + +#[cfg(all(target_arch = "mips", target_endian = "little"))] +include!("ioctl_mipsle.rs"); + +#[cfg(all(target_arch = "mips", target_endian = "big"))] +include!("ioctl_mipsbe.rs"); + +#[cfg(all(target_arch = "mips64", target_endian = "little"))] +include!("ioctl_mips64le.rs"); + +#[cfg(all(target_arch = "powerpc", target_endian = "little"))] +include!("ioctl_powerpcle.rs"); + +#[cfg(all(target_arch = "powerpc", target_endian = "big"))] +include!("ioctl_powerpcbe.rs"); + +#[cfg(all(target_arch = "powerpc64", target_endian = "little"))] +include!("ioctl_powerpc64le.rs"); + +#[cfg(all(target_arch = "powerpc64", target_endian = "big"))] +include!("ioctl_powerpc64be.rs"); + +#[cfg(all(target_arch = "arm", target_endian = "little"))] +include!("ioctl_armle.rs"); + +#[cfg(all(target_arch = "arm", target_endian = "big"))] +include!("ioctl_armbe.rs"); + +#[cfg(all(target_arch = "aarch64", target_endian = "little"))] +include!("ioctl_aarch64le.rs"); + +#[cfg(all(target_arch = "aarch64", target_endian = "big"))] +include!("ioctl_aarch64be.rs"); + +#[cfg(all(target_arch = "s390x", target_endian = "big"))] +include!("ioctl_s390xbe.rs"); + +#[cfg(all(target_arch = "riscv64", target_endian = "little"))] +include!("ioctl_riscv64.rs"); diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/mod.rs b/third_party/rust/authenticator/src/transport/linux/mod.rs new file mode 100644 index 0000000000..c4d490ecee --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/mod.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(clippy::unreadable_literal)] + +pub mod device; +pub mod transaction; + +mod hidraw; +mod hidwrapper; +mod monitor; diff --git a/third_party/rust/authenticator/src/transport/linux/monitor.rs b/third_party/rust/authenticator/src/transport/linux/monitor.rs new file mode 100644 index 0000000000..ee88622de9 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/monitor.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use libc::{c_int, c_short, c_ulong}; +use libudev::EventType; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::io; +use std::os::unix::io::AsRawFd; +use std::path::PathBuf; +use std::sync::{mpsc::Sender, Arc}; + +const UDEV_SUBSYSTEM: &str = "hidraw"; +const POLLIN: c_short = 0x0001; +const POLL_TIMEOUT: c_int = 100; + +fn poll(fds: &mut Vec<::libc::pollfd>) -> io::Result<()> { + let nfds = fds.len() as c_ulong; + + let rv = unsafe { ::libc::poll((fds[..]).as_mut_ptr(), nfds, POLL_TIMEOUT) }; + + if rv < 0 { + Err(io::Error::from_raw_os_error(rv)) + } else { + Ok(()) + } +} + +pub struct Monitor<F> +where + F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap<PathBuf, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + let ctx = libudev::Context::new()?; + + let mut enumerator = libudev::Enumerator::new(&ctx)?; + enumerator.match_subsystem(UDEV_SUBSYSTEM)?; + + // Iterate all existing devices. + let paths: Vec<PathBuf> = enumerator + .scan_devices()? + .filter_map(|dev| dev.devnode().map(|p| p.to_owned())) + .collect(); + + // Add them all in one go to avoid race conditions in DeviceSelector + // (8 devices should be added, but the first returns already before all + // others are known to DeviceSelector) + self.selector_sender + .send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?; + for path in paths { + self.add_device(path); + } + + let mut monitor = libudev::Monitor::new(&ctx)?; + monitor.match_subsystem(UDEV_SUBSYSTEM)?; + + // Start listening for new devices. + let mut socket = monitor.listen()?; + let mut fds = vec![::libc::pollfd { + fd: socket.as_raw_fd(), + events: POLLIN, + revents: 0, + }]; + + while alive() { + // Wait for new events, break on failure. + poll(&mut fds)?; + + if let Some(event) = socket.receive_event() { + self.process_event(&event); + } + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn process_event(&mut self, event: &libudev::Event) { + let path = event.device().devnode().map(|dn| dn.to_owned()); + + match (event.event_type(), path) { + (EventType::Add, Some(path)) => { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()])); + self.add_device(path); + } + (EventType::Remove, Some(path)) => { + self.remove_device(&path); + } + _ => { /* ignore other types and failures */ } + } + } + + fn add_device(&mut self, path: PathBuf) { + let f = self.new_device_cb.clone(); + let key = path.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + debug!("Adding device {}", path.to_string_lossy()); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: &PathBuf) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path.to_string_lossy()); + if let Some(runloop) = self.runloops.remove(path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(&path); + } + } +} + +pub fn get_property_linux(path: &PathBuf, prop_name: &str) -> io::Result<String> { + let ctx = libudev::Context::new()?; + + let mut enumerator = libudev::Enumerator::new(&ctx)?; + enumerator.match_subsystem(UDEV_SUBSYSTEM)?; + + // Iterate all existing devices, since we don't have a syspath + // and libudev-rs doesn't implement opening by devnode. + for dev in enumerator.scan_devices()? { + if dev.devnode().is_some() && dev.devnode().unwrap() == path { + debug!( + "get_property_linux Querying property {} from {}", + prop_name, + dev.syspath().display() + ); + + let value = dev + .attribute_value(prop_name) + .ok_or(io::ErrorKind::Other)? + .to_string_lossy(); + + debug!("get_property_linux Fetched Result, {}={}", prop_name, value); + return Ok(value.to_string()); + } + } + + Err(io::Error::new( + io::ErrorKind::Other, + "Unable to find device", + )) +} diff --git a/third_party/rust/authenticator/src/transport/linux/transaction.rs b/third_party/rust/authenticator/src/transport/linux/transaction.rs new file mode 100644 index 0000000000..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/transaction.rs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/macos/device.rs b/third_party/rust/authenticator/src/transport/macos/device.rs new file mode 100644 index 0000000000..0e55b92e96 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/device.rs @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate log; + +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::iokit::*; +use crate::transport::{FidoDevice, HIDError, SharedSecret}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use core_foundation::base::*; +use core_foundation::string::*; +use std::convert::TryInto; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::io; +use std::io::{Read, Write}; +use std::sync::mpsc::{Receiver, RecvTimeoutError}; +use std::time::Duration; + +const READ_TIMEOUT: u64 = 15; + +pub struct Device { + device_ref: IOHIDDeviceRef, + cid: [u8; 4], + report_rx: Option<Receiver<Vec<u8>>>, + dev_info: Option<U2FDeviceInfo>, + secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + unsafe fn get_property_macos(&self, prop_name: &str) -> io::Result<String> { + let prop_ref = IOHIDDeviceGetProperty( + self.device_ref, + CFString::new(prop_name).as_concrete_TypeRef(), + ); + if prop_ref.is_null() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("IOHIDDeviceGetProperty received nullptr for property {prop_name}"), + )); + } + + if CFGetTypeID(prop_ref) != CFStringGetTypeID() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("IOHIDDeviceGetProperty returned non-string type for property {prop_name}"), + )); + } + + Ok(CFString::from_void(prop_ref).to_string()) + } +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Device").field("cid", &self.cid).finish() + } +} + +impl PartialEq for Device { + fn eq(&self, other_device: &Device) -> bool { + self.device_ref == other_device.device_ref + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.device_ref.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, mut bytes: &mut [u8]) -> io::Result<usize> { + if let Some(rx) = &self.report_rx { + let timeout = Duration::from_secs(READ_TIMEOUT); + let data = match rx.recv_timeout(timeout) { + Ok(v) => v, + Err(e) if e == RecvTimeoutError::Timeout => { + return Err(io::Error::new(io::ErrorKind::TimedOut, e)); + } + Err(e) => { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, e)); + } + }; + bytes.write(&data) + } else { + Err(io::Error::from(io::ErrorKind::Unsupported)) + } + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + assert_eq!(bytes.len(), self.out_rpt_size() + 1); + + let report_id = i64::from(bytes[0]); + // Skip report number when not using numbered reports. + let start = if report_id == 0x0 { 1 } else { 0 }; + let data = &bytes[start..]; + + let result = unsafe { + IOHIDDeviceSetReport( + self.device_ref, + kIOHIDReportTypeOutput, + report_id.try_into().unwrap(), + data.as_ptr(), + data.len() as CFIndex, + ) + }; + if result != 0 { + warn!("set_report sending failure = {0:X}", result); + return Err(io::Error::from_raw_os_error(result)); + } + trace!("set_report sending success = {0:X}", result); + + Ok(bytes.len()) + } + + // USB HID writes don't buffer, so this will be a nop. + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + unsafe { self.get_property_macos(prop_name) } + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = (IOHIDDeviceRef, Receiver<Vec<u8>>); + type Id = IOHIDDeviceRef; + + fn new(dev_ids: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> { + let (device_ref, report_rx) = dev_ids; + Ok(Self { + device_ref, + cid: CID_BROADCAST, + report_rx: Some(report_rx), + dev_info: None, + secret: None, + authenticator_info: None, + }) + } + + fn initialized(&self) -> bool { + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.device_ref + } + + fn is_u2f(&mut self) -> bool { + true + } + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/macos/iokit.rs b/third_party/rust/authenticator/src/transport/macos/iokit.rs new file mode 100644 index 0000000000..656cdb045d --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/iokit.rs @@ -0,0 +1,292 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)] + +extern crate libc; + +use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID}; +use core_foundation::array::*; +use core_foundation::base::*; +use core_foundation::dictionary::*; +use core_foundation::number::*; +use core_foundation::runloop::*; +use core_foundation::string::*; +use std::ops::Deref; +use std::os::raw::c_void; + +type IOOptionBits = u32; + +pub type IOReturn = libc::c_int; + +pub type IOHIDManagerRef = *mut __IOHIDManager; +pub type IOHIDManagerOptions = IOOptionBits; + +pub type IOHIDDeviceCallback = extern "C" fn( + context: *mut c_void, + result: IOReturn, + sender: *mut c_void, + device: IOHIDDeviceRef, +); + +pub type IOHIDReportType = IOOptionBits; +pub type IOHIDReportCallback = extern "C" fn( + context: *mut c_void, + result: IOReturn, + sender: IOHIDDeviceRef, + report_type: IOHIDReportType, + report_id: u32, + report: *mut u8, + report_len: CFIndex, +); + +pub const kIOHIDManagerOptionNone: IOHIDManagerOptions = 0; + +pub const kIOHIDReportTypeOutput: IOHIDReportType = 1; + +#[repr(C)] +pub struct __IOHIDManager { + __private: c_void, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct IOHIDDeviceRef(*const c_void); + +unsafe impl Send for IOHIDDeviceRef {} +unsafe impl Sync for IOHIDDeviceRef {} + +pub struct SendableRunLoop(CFRunLoopRef); + +impl SendableRunLoop { + pub fn new(runloop: CFRunLoopRef) -> Self { + // Keep the CFRunLoop alive for as long as we are. + unsafe { CFRetain(runloop as *mut c_void) }; + + SendableRunLoop(runloop) + } +} + +unsafe impl Send for SendableRunLoop {} + +impl Deref for SendableRunLoop { + type Target = CFRunLoopRef; + + fn deref(&self) -> &CFRunLoopRef { + &self.0 + } +} + +impl Drop for SendableRunLoop { + fn drop(&mut self) { + unsafe { CFRelease(self.0 as *mut c_void) }; + } +} + +#[repr(C)] +pub struct CFRunLoopObserverContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: Option<extern "C" fn(info: *const c_void) -> *const c_void>, + pub release: Option<extern "C" fn(info: *const c_void)>, + pub copyDescription: Option<extern "C" fn(info: *const c_void) -> CFStringRef>, +} + +impl CFRunLoopObserverContext { + pub fn new(context: *mut c_void) -> Self { + Self { + version: 0 as CFIndex, + info: context, + retain: None, + release: None, + copyDescription: None, + } + } +} + +pub struct CFRunLoopEntryObserver { + observer: CFRunLoopObserverRef, + // Keep alive until the observer goes away. + context_ptr: *mut CFRunLoopObserverContext, +} + +impl CFRunLoopEntryObserver { + pub fn new(callback: CFRunLoopObserverCallBack, context: *mut c_void) -> Self { + let context = CFRunLoopObserverContext::new(context); + let context_ptr = Box::into_raw(Box::new(context)); + + let observer = unsafe { + CFRunLoopObserverCreate( + kCFAllocatorDefault, + kCFRunLoopEntry, + false as Boolean, + 0, + callback, + context_ptr, + ) + }; + + Self { + observer, + context_ptr, + } + } + + pub fn add_to_current_runloop(&self) { + unsafe { + CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.observer, kCFRunLoopDefaultMode) + }; + } +} + +impl Drop for CFRunLoopEntryObserver { + fn drop(&mut self) { + unsafe { + CFRelease(self.observer as *mut c_void); + + // Drop the CFRunLoopObserverContext. + let _ = Box::from_raw(self.context_ptr); + }; + } +} + +pub struct IOHIDDeviceMatcher { + pub dict: CFDictionary<CFString, CFNumber>, +} + +impl IOHIDDeviceMatcher { + pub fn new() -> Self { + let dict = CFDictionary::<CFString, CFNumber>::from_CFType_pairs(&[ + ( + CFString::from_static_string("DeviceUsage"), + CFNumber::from(i32::from(FIDO_USAGE_U2FHID)), + ), + ( + CFString::from_static_string("DeviceUsagePage"), + CFNumber::from(i32::from(FIDO_USAGE_PAGE)), + ), + ]); + Self { dict } + } +} + +#[link(name = "IOKit", kind = "framework")] +extern "C" { + // CFRunLoop + pub fn CFRunLoopObserverCreate( + allocator: CFAllocatorRef, + activities: CFOptionFlags, + repeats: Boolean, + order: CFIndex, + callout: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) -> CFRunLoopObserverRef; + + // IOHIDManager + pub fn IOHIDManagerCreate( + allocator: CFAllocatorRef, + options: IOHIDManagerOptions, + ) -> IOHIDManagerRef; + pub fn IOHIDManagerSetDeviceMatching(manager: IOHIDManagerRef, matching: CFDictionaryRef); + pub fn IOHIDManagerRegisterDeviceMatchingCallback( + manager: IOHIDManagerRef, + callback: IOHIDDeviceCallback, + context: *mut c_void, + ); + pub fn IOHIDManagerRegisterDeviceRemovalCallback( + manager: IOHIDManagerRef, + callback: IOHIDDeviceCallback, + context: *mut c_void, + ); + pub fn IOHIDManagerRegisterInputReportCallback( + manager: IOHIDManagerRef, + callback: IOHIDReportCallback, + context: *mut c_void, + ); + pub fn IOHIDManagerOpen(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn; + pub fn IOHIDManagerClose(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn; + pub fn IOHIDManagerScheduleWithRunLoop( + manager: IOHIDManagerRef, + runLoop: CFRunLoopRef, + runLoopMode: CFStringRef, + ); + + // IOHIDDevice + pub fn IOHIDDeviceSetReport( + device: IOHIDDeviceRef, + reportType: IOHIDReportType, + reportID: CFIndex, + report: *const u8, + reportLength: CFIndex, + ) -> IOReturn; + pub fn IOHIDDeviceGetProperty(device: IOHIDDeviceRef, key: CFStringRef) -> CFTypeRef; +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + use std::os::raw::c_void; + use std::ptr; + use std::sync::mpsc::{channel, Sender}; + use std::thread; + + extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) { + let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) }; + + // Send the current runloop to the receiver to unblock it. + let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() })); + } + + #[test] + fn test_sendable_runloop() { + let (tx, rx) = channel(); + + let thread = thread::spawn(move || { + // Send the runloop to the owning thread. + let context = &tx as *const _ as *mut c_void; + let obs = CFRunLoopEntryObserver::new(observe, context); + obs.add_to_current_runloop(); + + unsafe { + // We need some source for the runloop to run. + let manager = IOHIDManagerCreate(kCFAllocatorDefault, 0); + assert!(!manager.is_null()); + + IOHIDManagerScheduleWithRunLoop( + manager, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode, + ); + IOHIDManagerSetDeviceMatching(manager, ptr::null_mut()); + + let rv = IOHIDManagerOpen(manager, 0); + assert_eq!(rv, 0); + + // This will run until `CFRunLoopStop()` is called. + CFRunLoopRun(); + + let rv = IOHIDManagerClose(manager, 0); + assert_eq!(rv, 0); + + CFRelease(manager as *mut c_void); + } + }); + + // Block until we enter the CFRunLoop. + let runloop: SendableRunLoop = rx.recv().expect("failed to receive runloop"); + + // Stop the runloop. + unsafe { CFRunLoopStop(*runloop) }; + + // Stop the thread. + thread.join().expect("failed to join the thread"); + + // Try to stop the runloop again (without crashing). + unsafe { CFRunLoopStop(*runloop) }; + } +} diff --git a/third_party/rust/authenticator/src/transport/macos/mod.rs b/third_party/rust/authenticator/src/transport/macos/mod.rs new file mode 100644 index 0000000000..44e85094d0 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod iokit; +mod monitor; diff --git a/third_party/rust/authenticator/src/transport/macos/monitor.rs b/third_party/rust/authenticator/src/transport/macos/monitor.rs new file mode 100644 index 0000000000..32200ee7a4 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/monitor.rs @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +extern crate log; + +use crate::transport::device_selector::DeviceSelectorEvent; +use crate::transport::platform::iokit::*; +use crate::util::io_err; +use core_foundation::base::*; +use core_foundation::runloop::*; +use runloop::RunLoop; +use std::collections::HashMap; +use std::os::raw::c_void; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::{io, slice}; + +struct DeviceData { + tx: Sender<Vec<u8>>, + runloop: RunLoop, +} + +pub struct Monitor<F> +where + F: Fn( + (IOHIDDeviceRef, Receiver<Vec<u8>>), + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + manager: IOHIDManagerRef, + // Keep alive until the monitor goes away. + _matcher: IOHIDDeviceMatcher, + map: HashMap<IOHIDDeviceRef, DeviceData>, + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn( + (IOHIDDeviceRef, Receiver<Vec<u8>>), + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) }; + + // Match FIDO devices only. + let _matcher = IOHIDDeviceMatcher::new(); + unsafe { IOHIDManagerSetDeviceMatching(manager, _matcher.dict.as_concrete_TypeRef()) }; + + Self { + manager, + _matcher, + new_device_cb, + map: HashMap::new(), + selector_sender, + status_sender, + } + } + + pub fn start(&mut self) -> io::Result<()> { + let context = self as *mut Self as *mut c_void; + + unsafe { + IOHIDManagerRegisterDeviceMatchingCallback( + self.manager, + Monitor::<F>::on_device_matching, + context, + ); + IOHIDManagerRegisterDeviceRemovalCallback( + self.manager, + Monitor::<F>::on_device_removal, + context, + ); + IOHIDManagerRegisterInputReportCallback( + self.manager, + Monitor::<F>::on_input_report, + context, + ); + + IOHIDManagerScheduleWithRunLoop( + self.manager, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode, + ); + + let rv = IOHIDManagerOpen(self.manager, kIOHIDManagerOptionNone); + if rv == 0 { + Ok(()) + } else { + Err(io_err(&format!("Couldn't open HID Manager, rv={rv}"))) + } + } + } + + pub fn stop(&mut self) { + // Remove all devices. + while !self.map.is_empty() { + let device_ref = *self.map.keys().next().unwrap(); + self.remove_device(device_ref); + } + + // Close the manager and its devices. + unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) }; + } + + fn remove_device(&mut self, device_ref: IOHIDDeviceRef) { + if let Some(DeviceData { tx, runloop }) = self.map.remove(&device_ref) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(device_ref)); + // Dropping `tx` will make Device::read() fail eventually. + drop(tx); + + // Wait until the runloop stopped. + runloop.cancel(); + } + } + + extern "C" fn on_input_report( + context: *mut c_void, + _: IOReturn, + device_ref: IOHIDDeviceRef, + _: IOHIDReportType, + _: u32, + report: *mut u8, + report_len: CFIndex, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + let mut send_failed = false; + + // Ignore the report if we can't find a device for it. + if let Some(DeviceData { tx, .. }) = this.map.get(&device_ref) { + let data = unsafe { slice::from_raw_parts(report, report_len as usize).to_vec() }; + send_failed = tx.send(data).is_err(); + } + + // Remove the device if sending fails. + if send_failed { + this.remove_device(device_ref); + } + } + + extern "C" fn on_device_matching( + context: *mut c_void, + _: IOReturn, + _: *mut c_void, + device_ref: IOHIDDeviceRef, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + let _ = this + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![device_ref])); + let selector_sender = this.selector_sender.clone(); + let status_sender = this.status_sender.clone(); + let (tx, rx) = channel(); + let f = &this.new_device_cb; + + // Create a new per-device runloop. + let runloop = RunLoop::new(move |alive| { + // Ensure that the runloop is still alive. + if alive() { + f((device_ref, rx), selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + this.map.insert(device_ref, DeviceData { tx, runloop }); + } + } + + extern "C" fn on_device_removal( + context: *mut c_void, + _: IOReturn, + _: *mut c_void, + device_ref: IOHIDDeviceRef, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + this.remove_device(device_ref); + } +} + +impl<F> Drop for Monitor<F> +where + F: Fn( + (IOHIDDeviceRef, Receiver<Vec<u8>>), + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + fn drop(&mut self) { + unsafe { CFRelease(self.manager as *mut c_void) }; + } +} diff --git a/third_party/rust/authenticator/src/transport/macos/transaction.rs b/third_party/rust/authenticator/src/transport/macos/transaction.rs new file mode 100644 index 0000000000..d9709e7364 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/transaction.rs @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::iokit::{CFRunLoopEntryObserver, SendableRunLoop}; +use crate::transport::platform::monitor::Monitor; +use core_foundation::runloop::*; +use std::os::raw::c_void; +use std::sync::mpsc::{channel, Sender}; +use std::thread; + +// A transaction will run the given closure in a new thread, thereby using a +// separate per-thread state machine for each HID. It will either complete or +// fail through user action, timeout, or be cancelled when overridden by a new +// transaction. +pub struct Transaction { + runloop: Option<SendableRunLoop>, + thread: Option<thread::JoinHandle<()>>, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let (tx, rx) = channel(); + let timeout = (timeout as f64) / 1000.0; + let device_selector = DeviceSelector::run(); + let selector_sender = device_selector.clone_sender(); + let builder = thread::Builder::new(); + let thread = builder + .spawn(move || { + // Add a runloop observer that will be notified when we enter the + // runloop and tx.send() the current runloop to the owning thread. + // We need to ensure the runloop was entered before unblocking + // Transaction::new(), so we can always properly cancel. + let context = &tx as *const _ as *mut c_void; + let obs = CFRunLoopEntryObserver::new(Transaction::observe, context); + obs.add_to_current_runloop(); + + // Create a new HID device monitor and start polling. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status); + try_or!(monitor.start(), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // This will block until completion, abortion, or timeout. + unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, 0) }; + + // Close the monitor and its devices. + monitor.stop(); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + // Block until we enter the CFRunLoop. + let runloop = rx + .recv() + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + runloop: Some(runloop), + thread: Some(thread), + device_selector, + }) + } + + extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) { + let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) }; + + // Send the current runloop to the receiver to unblock it. + let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() })); + } + + pub fn cancel(&mut self) { + // This must never be None. This won't block. + unsafe { CFRunLoopStop(*self.runloop.take().unwrap()) }; + + self.device_selector.stop(); + // This must never be None. Ignore return value. + let _ = self.thread.take().unwrap().join(); + } +} diff --git a/third_party/rust/authenticator/src/transport/mock/device.rs b/third_party/rust/authenticator/src/transport/mock/device.rs new file mode 100644 index 0000000000..c22e53b3bd --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mock/device.rs @@ -0,0 +1,181 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use crate::consts::CID_BROADCAST; +use crate::crypto::SharedSecret; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::device_selector::DeviceCommand; +use crate::transport::{hid::HIDDevice, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::sync::mpsc::{channel, Receiver, Sender}; + +pub(crate) const IN_HID_RPT_SIZE: usize = 64; +const OUT_HID_RPT_SIZE: usize = 64; + +#[derive(Debug)] +pub struct Device { + pub id: String, + pub cid: [u8; 4], + pub reads: Vec<[u8; IN_HID_RPT_SIZE]>, + pub writes: Vec<[u8; OUT_HID_RPT_SIZE + 1]>, + pub dev_info: Option<U2FDeviceInfo>, + pub authenticator_info: Option<AuthenticatorInfo>, + pub sender: Option<Sender<DeviceCommand>>, + pub receiver: Option<Receiver<DeviceCommand>>, +} + +impl Device { + pub fn add_write(&mut self, packet: &[u8], fill_value: u8) { + // Add one to deal with record index check + let mut write = [fill_value; OUT_HID_RPT_SIZE + 1]; + // Make sure we start with a 0, for HID record index + write[0] = 0; + // Clone packet data in at 1, since front is padded with HID record index + write[1..=packet.len()].clone_from_slice(packet); + self.writes.push(write); + } + + pub fn add_read(&mut self, packet: &[u8], fill_value: u8) { + let mut read = [fill_value; IN_HID_RPT_SIZE]; + read[..packet.len()].clone_from_slice(packet); + self.reads.push(read); + } + + pub fn create_channel(&mut self) { + let (tx, rx) = channel(); + self.sender = Some(tx); + self.receiver = Some(rx); + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + // Pop a vector from the expected writes, check for quality + // against bytes array. + assert!( + !self.writes.is_empty(), + "Ran out of expected write values! Wanted to write {:?}", + bytes + ); + let check = self.writes.remove(0); + assert_eq!(check.len(), bytes.len()); + assert_eq!(&check, bytes); + Ok(bytes.len()) + } + + // nop + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl Read for Device { + fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> { + assert!(!self.reads.is_empty(), "Ran out of read values!"); + let check = self.reads.remove(0); + assert_eq!(check.len(), bytes.len()); + bytes.clone_from_slice(&check); + Ok(check.len()) + } +} + +impl Drop for Device { + fn drop(&mut self) { + if !std::thread::panicking() { + assert!(self.reads.is_empty()); + assert!(self.writes.is_empty()); + } + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.id == other.id + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + IN_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + OUT_HID_RPT_SIZE + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + Ok(format!("{prop_name} not implemented")) + } + fn get_device_info(&self) -> U2FDeviceInfo { + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type Id = String; + type BuildParameters = &'static str; // None used + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + fn set_shared_secret(&mut self, _: SharedSecret) { + // Nothing + } + fn get_shared_secret(&self) -> std::option::Option<&SharedSecret> { + None + } + + fn new(id: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> { + Ok(Device { + id: id.to_string(), + cid: CID_BROADCAST, + reads: vec![], + writes: vec![], + dev_info: None, + authenticator_info: None, + sender: None, + receiver: None, + }) + } + + fn initialized(&self) -> bool { + self.get_cid() != &CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.id.clone() + } + + fn is_u2f(&mut self) -> bool { + self.sender.is_some() + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/mock/mod.rs b/third_party/rust/authenticator/src/transport/mock/mod.rs new file mode 100644 index 0000000000..d0e200a7ef --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mock/mod.rs @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; diff --git a/third_party/rust/authenticator/src/transport/mock/transaction.rs b/third_party/rust/authenticator/src/transport/mock/transaction.rs new file mode 100644 index 0000000000..e19b1cb56f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mock/transaction.rs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{DeviceBuildParameters, DeviceSelectorEvent}; +use std::sync::mpsc::Sender; + +pub struct Transaction {} + +impl Transaction { + pub fn new<F, T>( + _timeout: u64, + _callback: StateCallback<crate::Result<T>>, + _status: Sender<crate::StatusUpdate>, + _new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + Ok(Self {}) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + } +} diff --git a/third_party/rust/authenticator/src/transport/mod.rs b/third_party/rust/authenticator/src/transport/mod.rs new file mode 100644 index 0000000000..91ec7fe1d7 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mod.rs @@ -0,0 +1,334 @@ +use crate::consts::{Capability, HIDCmd}; +use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret}; +use crate::ctap2::commands::client_pin::{ + GetKeyAgreement, GetPinToken, GetPinUvAuthTokenUsingPinWithPermissions, + GetPinUvAuthTokenUsingUvWithPermissions, PinUvAuthTokenPermission, +}; +use crate::ctap2::commands::get_info::{AuthenticatorVersion, GetInfo}; +use crate::ctap2::commands::get_version::GetVersion; +use crate::ctap2::commands::make_credentials::dummy_make_credentials_cmd; +use crate::ctap2::commands::selection::Selection; +use crate::ctap2::commands::{ + CommandError, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode, +}; +use crate::transport::device_selector::BlinkResult; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::hid::HIDDevice; +use crate::util::io_err; +use crate::Pin; +use std::convert::TryFrom; +use std::thread; +use std::time::Duration; + +pub mod device_selector; +pub mod errors; +pub mod hid; + +#[cfg(all( + any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"), + not(test) +))] +pub mod hidproto; + +#[cfg(all(target_os = "linux", not(test)))] +#[path = "linux/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "freebsd", not(test)))] +#[path = "freebsd/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "netbsd", not(test)))] +#[path = "netbsd/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "openbsd", not(test)))] +#[path = "openbsd/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "macos", not(test)))] +#[path = "macos/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "windows", not(test)))] +#[path = "windows/mod.rs"] +pub mod platform; + +#[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + test +)))] +#[path = "stub/mod.rs"] +pub mod platform; + +#[cfg(test)] +#[path = "mock/mod.rs"] +pub mod platform; + +#[derive(Debug)] +pub enum Nonce { + CreateRandom, + Use([u8; 8]), +} + +// TODO(MS): This is the lazy way: FidoDevice currently only extends HIDDevice by more functions, +// but the goal is to remove U2FDevice entirely and copy over the trait-definition here +pub trait FidoDevice: HIDDevice { + fn send_msg<Out, Req: Request<Out>>(&mut self, msg: &Req) -> Result<Out, HIDError> { + self.send_msg_cancellable(msg, &|| true) + } + + fn send_cbor<Req: RequestCtap2>(&mut self, msg: &Req) -> Result<Req::Output, HIDError> { + self.send_cbor_cancellable(msg, &|| true) + } + + fn send_ctap1<Req: RequestCtap1>(&mut self, msg: &Req) -> Result<Req::Output, HIDError> { + self.send_ctap1_cancellable(msg, &|| true) + } + + fn send_msg_cancellable<Out, Req: Request<Out>>( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result<Out, HIDError> { + if !self.initialized() { + return Err(HIDError::DeviceNotInitialized); + } + + if self.get_authenticator_info().is_some() { + self.send_cbor_cancellable(msg, keep_alive) + } else { + self.send_ctap1_cancellable(msg, keep_alive) + } + } + + fn send_cbor_cancellable<Req: RequestCtap2>( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result<Req::Output, HIDError> { + debug!("sending {:?} to {:?}", msg, self); + + let mut data = msg.wire_format()?; + let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1); + // CTAP2 command + buf.push(Req::command() as u8); + // payload + buf.append(&mut data); + let buf = buf; + + let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf, keep_alive)?; + debug!( + "got from Device {:?} status={:?}: {:?}", + self.id(), + cmd, + resp + ); + if cmd == HIDCmd::Cbor { + Ok(msg.handle_response_ctap2(self, &resp)?) + } else { + Err(HIDError::UnexpectedCmd(cmd.into())) + } + } + + fn send_ctap1_cancellable<Req: RequestCtap1>( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result<Req::Output, HIDError> { + debug!("sending {:?} to {:?}", msg, self); + let (data, add_info) = msg.ctap1_format()?; + + while keep_alive() { + // sendrecv will not block with a CTAP1 device + let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data, &|| true)?; + debug!( + "got from Device {:?} status={:?}: {:?}", + self.id(), + cmd, + data + ); + if cmd == HIDCmd::Msg { + if data.len() < 2 { + return Err(io_err("Unexpected Response: shorter than expected").into()); + } + let split_at = data.len() - 2; + let status = data.split_off(split_at); + // This will bubble up error if status != no error + let status = ApduErrorStatus::from([status[0], status[1]]); + + match msg.handle_response_ctap1(status, &data, &add_info) { + Ok(out) => return Ok(out), + Err(Retryable::Retry) => { + // sleep 100ms then loop again + // TODO(baloo): meh, use tokio instead? + thread::sleep(Duration::from_millis(100)); + } + Err(Retryable::Error(e)) => return Err(e), + } + } else { + return Err(HIDError::UnexpectedCmd(cmd.into())); + } + } + + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::KeepaliveCancel, + None, + ))) + } + + // This is ugly as we have 2 init-functions now, but the fastest way currently. + fn init(&mut self, nonce: Nonce) -> Result<(), HIDError> { + <Self as HIDDevice>::initialize(self, nonce)?; + + // If the device has the CBOR capability flag, then we'll check + // for CTAP2 support by sending an authenticatorGetInfo command. + // We're not aware of any CTAP2 devices that fail to set the CBOR + // capability flag, but we may need to rework this in the future. + if self.get_device_info().cap_flags.contains(Capability::CBOR) { + let command = GetInfo::default(); + if let Ok(info) = self.send_cbor(&command) { + debug!("{:?}: {:?}", self.id(), info); + if info.max_supported_version() != AuthenticatorVersion::U2F_V2 { + // Device supports CTAP2 + self.set_authenticator_info(info); + return Ok(()); + } + } + // An error from GetInfo might indicate that we're talking + // to a CTAP1 device that mistakenly claimed the CBOR capability, + // so we fallthrough here. + } + // We want to return an error here if this device doesn't support CTAP1, + // so we send a U2F_VERSION command. + let command = GetVersion::default(); + self.send_ctap1(&command)?; + Ok(()) + } + + fn block_and_blink(&mut self, keep_alive: &dyn Fn() -> bool) -> BlinkResult { + let supports_select_cmd = self.get_authenticator_info().map_or(false, |i| { + i.versions.contains(&AuthenticatorVersion::FIDO_2_1) + }); + let resp = if supports_select_cmd { + let msg = Selection {}; + self.send_cbor_cancellable(&msg, keep_alive) + } else { + // We need to fake a blink-request, because FIDO2.0 forgot to specify one + // See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential + let msg = dummy_make_credentials_cmd(); + info!("Trying to blink: {:?}", &msg); + // We don't care about the Ok-value, just if it is Ok or not + self.send_msg_cancellable(&msg, keep_alive).map(|_| ()) + }; + + match resp { + // Spec only says PinInvalid or PinNotSet should be returned on the fake touch-request, + // but Yubikeys for example return PinAuthInvalid. A successful return is also possible + // for CTAP1-tokens so we catch those here as well. + Ok(_) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => { + BlinkResult::DeviceSelected + } + // We cancelled the receive, because another device was selected. + Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => { + // TODO: Repeat the request, if it is a UserActionTimeout? + debug!("Device {:?} got cancelled", &self); + BlinkResult::Cancelled + } + // Something unexpected happened, so we assume this device is not usable and + // interpreting this equivalent to being cancelled. + e => { + info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e); + BlinkResult::Cancelled + } + } + } + + fn establish_shared_secret(&mut self) -> Result<SharedSecret, HIDError> { + // CTAP1 devices don't support establishing a shared secret + let info = match self.get_authenticator_info() { + Some(info) => info, + None => return Err(HIDError::UnsupportedCommand), + }; + + let pin_protocol = PinUvAuthProtocol::try_from(info)?; + + // Not reusing the shared secret here, if it exists, since we might start again + // with a different PIN (e.g. if the last one was wrong) + let pin_command = GetKeyAgreement::new(pin_protocol); + let device_key_agreement = self.send_cbor(&pin_command)?; + let shared_secret = device_key_agreement.shared_secret()?; + self.set_shared_secret(shared_secret.clone()); + Ok(shared_secret) + } + + /// CTAP 2.0-only version: + /// "Getting pinUvAuthToken using getPinToken (superseded)" + fn get_pin_token(&mut self, pin: &Option<Pin>) -> Result<PinUvAuthToken, HIDError> { + // Asking the user for PIN before establishing the shared secret + let pin = pin + .as_ref() + .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?; + + // Not reusing the shared secret here, if it exists, since we might start again + // with a different PIN (e.g. if the last one was wrong) + let shared_secret = self.establish_shared_secret()?; + + let pin_command = GetPinToken::new(&shared_secret, pin); + let pin_token = self.send_cbor(&pin_command)?; + + Ok(pin_token) + } + + fn get_pin_uv_auth_token_using_uv_with_permissions( + &mut self, + permission: PinUvAuthTokenPermission, + rp_id: Option<&String>, + ) -> Result<PinUvAuthToken, HIDError> { + // Explicitly not reusing the shared secret here + let shared_secret = self.establish_shared_secret()?; + let pin_command = GetPinUvAuthTokenUsingUvWithPermissions::new( + &shared_secret, + permission, + rp_id.cloned(), + ); + let pin_auth_token = self.send_cbor(&pin_command)?; + + Ok(pin_auth_token) + } + + fn get_pin_uv_auth_token_using_pin_with_permissions( + &mut self, + pin: &Option<Pin>, + permission: PinUvAuthTokenPermission, + rp_id: Option<&String>, + ) -> Result<PinUvAuthToken, HIDError> { + // Asking the user for PIN before establishing the shared secret + let pin = pin + .as_ref() + .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?; + + // Not reusing the shared secret here, if it exists, since we might start again + // with a different PIN (e.g. if the last one was wrong) + let shared_secret = self.establish_shared_secret()?; + let pin_command = GetPinUvAuthTokenUsingPinWithPermissions::new( + &shared_secret, + pin, + permission, + rp_id.cloned(), + ); + let pin_auth_token = self.send_cbor(&pin_command)?; + + Ok(pin_auth_token) + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/device.rs b/third_party/rust/authenticator/src/transport/netbsd/device.rs new file mode 100644 index 0000000000..c93aee8d6a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/device.rs @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::fd::Fd; +use crate::transport::platform::monitor::WrappedOpenDevice; +use crate::transport::platform::uhid; +use crate::transport::{FidoDevice, HIDError, SharedSecret}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::io_err; +use std::ffi::OsString; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::mem; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: Fd, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + fn ping(&mut self) -> io::Result<()> { + for i in 0..10 { + let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE]; + + buf[0] = 0; // report number + buf[1] = 0xff; // CID_BROADCAST + buf[2] = 0xff; + buf[3] = 0xff; + buf[4] = 0xff; + buf[5] = 0x81; // ping + buf[6] = 0; + buf[7] = 1; // one byte + + // Write ping request. Each write to the device contains + // exactly one report id byte[*] followed by exactly as + // many bytes as are in a report, and will be consumed all + // at once by /dev/uhidN. So we use plain write, not + // write_all to issue writes in a loop. + // + // [*] This is only for the internal authenticator-rs API, + // not for the USB HID protocol, which for a device with + // only one report id excludes the report id byte from the + // interrupt in/out pipe transfer format. + if self.write(&buf)? != buf.len() { + return Err(io_err("write ping failed")); + } + + // Wait for response + let mut pfd: libc::pollfd = unsafe { mem::zeroed() }; + pfd.fd = self.fd.fileno; + pfd.events = libc::POLLIN; + let nfds = unsafe { libc::poll(&mut pfd, 1, 100) }; + if nfds == -1 { + return Err(io::Error::last_os_error()); + } + if nfds == 0 { + debug!("device timeout {}", i); + continue; + } + + // Read response. When reports come in they are all + // exactly the same size, with no report id byte because + // there is only one report. + let n = self.read(&mut buf[1..])?; + if n != buf.len() - 1 { + return Err(io_err("read pong failed")); + } + + return Ok(()); + } + + Err(io_err("no response from device")) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.fd == other.fd + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + self.fd.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let bufp = buf.as_mut_ptr() as *mut libc::c_void; + let nread = unsafe { libc::read(self.fd.fileno, bufp, buf.len()) }; + if nread == -1 { + return Err(io::Error::last_os_error()); + } + Ok(nread as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + // Always skip the first byte (report number) + let data = &buf[1..]; + let data_ptr = data.as_ptr() as *const libc::c_void; + let nwrit = unsafe { libc::write(self.fd.fileno, data_ptr, data.len()) }; + if nwrit == -1 { + return Err(io::Error::last_os_error()); + } + // Pretend we wrote the report number byte + Ok(nwrit as usize + 1) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = WrappedOpenDevice; + type Id = OsString; + + fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> { + debug!("device found: {:?}", fido); + let mut res = Self { + path: fido.os_path, + fd: fido.fd, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + if !uhid::is_u2f_device(&self.fd) { + return false; + } + // This step is not strictly necessary -- NetBSD puts fido + // devices into raw mode automatically by default, but in + // principle that might change, and this serves as a test to + // verify that we're running on a kernel with support for raw + // mode at all so we don't get confused issuing writes that try + // to set the report descriptor rather than transfer data on + // the output interrupt pipe as we need. + match uhid::hid_set_raw(&self.fd, true) { + Ok(_) => (), + Err(_) => return false, + } + if self.ping().is_err() { + return false; + } + true + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/netbsd/fd.rs b/third_party/rust/authenticator/src/transport/netbsd/fd.rs new file mode 100644 index 0000000000..d45410843b --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/fd.rs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use std::ffi::CString; +use std::ffi::OsStr; +use std::hash::{Hash, Hasher}; +use std::io; +use std::mem; +use std::os::raw::c_int; +use std::os::unix::{ffi::OsStrExt, io::RawFd}; + +#[derive(Debug)] +pub struct Fd { + pub fileno: RawFd, +} + +impl Fd { + pub fn open(path: &OsStr, flags: c_int) -> io::Result<Fd> { + let cpath = CString::new(path.as_bytes())?; + let rv = unsafe { libc::open(cpath.as_ptr(), flags) }; + if rv == -1 { + return Err(io::Error::last_os_error()); + } + Ok(Fd { fileno: rv }) + } +} + +impl Drop for Fd { + fn drop(&mut self) { + unsafe { libc::close(self.fileno) }; + } +} + +impl PartialEq for Fd { + fn eq(&self, other: &Fd) -> bool { + let mut st: libc::stat = unsafe { mem::zeroed() }; + let mut sto: libc::stat = unsafe { mem::zeroed() }; + if unsafe { libc::fstat(self.fileno, &mut st) } == -1 { + return false; + } + if unsafe { libc::fstat(other.fileno, &mut sto) } == -1 { + return false; + } + (st.st_dev == sto.st_dev) & (st.st_ino == sto.st_ino) + } +} + +impl Eq for Fd {} + +impl Hash for Fd { + fn hash<H: Hasher>(&self, state: &mut H) { + let mut st: libc::stat = unsafe { mem::zeroed() }; + if unsafe { libc::fstat(self.fileno, &mut st) } == -1 { + return; + } + st.st_dev.hash(state); + st.st_ino.hash(state); + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/mod.rs b/third_party/rust/authenticator/src/transport/netbsd/mod.rs new file mode 100644 index 0000000000..a0eabb6e06 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/mod.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod fd; +mod monitor; +mod uhid; diff --git a/third_party/rust/authenticator/src/transport/netbsd/monitor.rs b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs new file mode 100644 index 0000000000..c521bdea8b --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use crate::transport::platform::fd::Fd; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::ffi::OsString; +use std::sync::{mpsc::Sender, Arc}; +use std::thread; +use std::time::Duration; + +// XXX Should use drvctl, but it doesn't do pubsub properly yet so +// DRVGETEVENT requires write access to /dev/drvctl. Instead, for now, +// just poll every 500ms. +const POLL_TIMEOUT: u64 = 500; + +#[derive(Debug)] +pub struct WrappedOpenDevice { + pub fd: Fd, + pub os_path: OsString, +} + +pub struct Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + // Loop until we're stopped by the controlling thread, or fail. + while alive() { + for n in 0..100 { + let uhidpath = OsString::from(format!("/dev/uhid{n}")); + match Fd::open(&uhidpath, libc::O_RDWR | libc::O_CLOEXEC) { + Ok(uhid) => { + // The device is available if it can be opened. + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![uhidpath.clone()])); + self.add_device(WrappedOpenDevice { + fd: uhid, + os_path: uhidpath, + }); + } + Err(ref err) => match err.raw_os_error() { + Some(libc::EBUSY) => continue, + Some(libc::ENOENT) => break, + _ => self.remove_device(uhidpath), + }, + } + } + thread::sleep(Duration::from_millis(POLL_TIMEOUT)); + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn add_device(&mut self, fido: WrappedOpenDevice) { + let f = self.new_device_cb.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + let key = fido.os_path.clone(); + debug!("Adding device {}", key.to_string_lossy()); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(fido, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: OsString) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path.to_string_lossy()); + if let Some(runloop) = self.runloops.remove(&path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/transaction.rs b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs new file mode 100644 index 0000000000..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/uhid.rs b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs new file mode 100644 index 0000000000..ea183db998 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use std::io; +use std::mem; +use std::os::raw::c_int; +use std::os::raw::c_uchar; + +use crate::transport::hidproto::has_fido_usage; +use crate::transport::hidproto::ReportDescriptor; +use crate::transport::platform::fd::Fd; +use crate::util::io_err; + +/* sys/ioccom.h */ + +const IOCPARM_MASK: u32 = 0x1fff; +const IOCPARM_SHIFT: u32 = 16; +const IOCGROUP_SHIFT: u32 = 8; + +//const IOC_VOID: u32 = 0x20000000; +const IOC_OUT: u32 = 0x40000000; +const IOC_IN: u32 = 0x80000000; +//const IOC_INOUT: u32 = IOC_IN|IOC_OUT; + +macro_rules! ioctl { + ($dir:expr, $name:ident, $group:expr, $nr:expr, $ty:ty) => { + unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> { + let ioc = ($dir as u32) + | ((mem::size_of::<$ty>() as u32 & IOCPARM_MASK) << IOCPARM_SHIFT) + | (($group as u32) << IOCGROUP_SHIFT) + | ($nr as u32); + let rv = libc::ioctl(fd, ioc as libc::c_ulong, val); + if rv == -1 { + return Err(io::Error::last_os_error()); + } + Ok(rv) + } + }; +} + +#[allow(non_camel_case_types)] +#[repr(C)] +struct usb_ctl_report_desc { + ucrd_size: c_int, + ucrd_data: [c_uchar; 1024], +} + +ioctl!(IOC_OUT, usb_get_report_desc, b'U', 21, usb_ctl_report_desc); + +fn read_report_descriptor(fd: &Fd) -> io::Result<ReportDescriptor> { + let mut desc = unsafe { mem::zeroed() }; + unsafe { usb_get_report_desc(fd.fileno, &mut desc) }?; + if desc.ucrd_size < 0 { + return Err(io_err("negative report descriptor size")); + } + let size = desc.ucrd_size as usize; + let value = Vec::from(&desc.ucrd_data[..size]); + Ok(ReportDescriptor { value }) +} + +pub fn is_u2f_device(fd: &Fd) -> bool { + match read_report_descriptor(fd) { + Ok(desc) => has_fido_usage(desc), + Err(_) => false, + } +} + +ioctl!(IOC_IN, usb_hid_set_raw_ioctl, b'h', 2, c_int); + +pub fn hid_set_raw(fd: &Fd, raw: bool) -> io::Result<()> { + let mut raw_int: c_int = if raw { 1 } else { 0 }; + unsafe { usb_hid_set_raw_ioctl(fd.fileno, &mut raw_int) }?; + Ok(()) +} diff --git a/third_party/rust/authenticator/src/transport/openbsd/device.rs b/third_party/rust/authenticator/src/transport/openbsd/device.rs new file mode 100644 index 0000000000..fe4d6a642e --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/device.rs @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::monitor::WrappedOpenDevice; +use crate::transport::{FidoDevice, HIDError, SharedSecret}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::{from_unix_result, io_err}; +use std::ffi::{CString, OsString}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::mem; +use std::os::unix::ffi::OsStrExt; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: libc::c_int, + in_rpt_size: usize, + out_rpt_size: usize, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + fn ping(&mut self) -> io::Result<()> { + let capacity = 256; + + for _ in 0..10 { + let mut data = vec![0u8; capacity]; + + // Send 1 byte ping + // self.write_all requires Device to be mut. This can't be done at the moment, + // and this is a workaround anyways, so writing by hand instead. + self.write_all(&[0, 0xff, 0xff, 0xff, 0xff, 0x81, 0, 1])?; + + // Wait for response + let mut pfd: libc::pollfd = unsafe { mem::zeroed() }; + pfd.fd = self.fd; + pfd.events = libc::POLLIN; + if from_unix_result(unsafe { libc::poll(&mut pfd, 1, 100) })? == 0 { + debug!("device {:?} timeout", self.path); + continue; + } + + // Read response + self.read(&mut data[..])?; + + return Ok(()); + } + + Err(io_err("no response from device")) + } +} + +impl Drop for Device { + fn drop(&mut self) { + // Close the fd, ignore any errors. + let _ = unsafe { libc::close(self.fd) }; + debug!("device {:?} closed", self.path); + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let buf_ptr = buf.as_mut_ptr() as *mut libc::c_void; + let rv = unsafe { libc::read(self.fd, buf_ptr, buf.len()) }; + from_unix_result(rv as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + // Always skip the first byte (report number) + let data = &buf[1..]; + let data_ptr = data.as_ptr() as *const libc::c_void; + let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) }; + Ok(from_unix_result(rv as usize)? + 1) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + self.in_rpt_size + } + + fn out_rpt_size(&self) -> usize { + self.out_rpt_size + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = WrappedOpenDevice; + type Id = OsString; + + fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> { + debug!("device found: {:?}", fido); + let mut res = Self { + path: fido.os_path, + fd: fido.fd, + in_rpt_size: MAX_HID_RPT_SIZE, + out_rpt_size: MAX_HID_RPT_SIZE, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + debug!("device {:?} is U2F/FIDO", self.path); + + // From OpenBSD's libfido2 in 6.6-current: + // "OpenBSD (as of 201910) has a bug that causes it to lose + // track of the DATA0/DATA1 sequence toggle across uhid device + // open and close. This is a terrible hack to work around it." + match self.ping() { + Ok(_) => true, + Err(err) => { + debug!("device {:?} is not responding: {}", self.path, err); + false + } + } + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/openbsd/mod.rs b/third_party/rust/authenticator/src/transport/openbsd/mod.rs new file mode 100644 index 0000000000..fa02132e67 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/mod.rs @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod monitor; diff --git a/third_party/rust/authenticator/src/transport/openbsd/monitor.rs b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs new file mode 100644 index 0000000000..0ea5f3d0b8 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use crate::util::from_unix_result; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::ffi::{CString, OsString}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::io::RawFd; +use std::path::PathBuf; +use std::sync::{mpsc::Sender, Arc}; +use std::thread; +use std::time::Duration; + +const POLL_TIMEOUT: u64 = 500; + +#[derive(Debug)] +pub struct WrappedOpenDevice { + pub fd: RawFd, + pub os_path: OsString, +} + +pub struct Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + // Loop until we're stopped by the controlling thread, or fail. + while alive() { + // Iterate the first 10 fido(4) devices. + for path in (0..10) + .map(|unit| PathBuf::from(&format!("/dev/fido/{}", unit))) + .filter(|path| path.exists()) + { + let os_path = path.as_os_str().to_os_string(); + let cstr = CString::new(os_path.as_bytes())?; + + // Try to open the device. + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) }; + match from_unix_result(fd) { + Ok(fd) => { + // The device is available if it can be opened. + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![os_path.clone()])); + self.add_device(WrappedOpenDevice { fd, os_path }); + } + Err(ref err) if err.raw_os_error() == Some(libc::EBUSY) => { + // The device is available but currently in use. + } + _ => { + // libc::ENODEV or any other error. + self.remove_device(os_path); + } + } + } + + thread::sleep(Duration::from_millis(POLL_TIMEOUT)); + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn add_device(&mut self, fido: WrappedOpenDevice) { + if !self.runloops.contains_key(&fido.os_path) { + let f = self.new_device_cb.clone(); + let key = fido.os_path.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + let runloop = RunLoop::new(move |alive| { + if alive() { + f(fido, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + } + + fn remove_device(&mut self, path: OsString) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + if let Some(runloop) = self.runloops.remove(&path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/openbsd/transaction.rs b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs new file mode 100644 index 0000000000..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/stub/device.rs b/third_party/rust/authenticator/src/transport/stub/device.rs new file mode 100644 index 0000000000..9c5a412a95 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/stub/device.rs @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::FidoDevice; +use crate::transport::{HIDError, SharedSecret}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use std::hash::Hash; +use std::io; +use std::io::{Read, Write}; +use std::path::PathBuf; + +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct Device {} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + panic!("not implemented"); + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + panic!("not implemented"); + } + + fn flush(&mut self) -> io::Result<()> { + panic!("not implemented"); + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + panic!("not implemented"); + } + + fn set_cid(&mut self, cid: [u8; 4]) { + panic!("not implemented"); + } + + fn in_rpt_size(&self) -> usize { + panic!("not implemented"); + } + + fn out_rpt_size(&self) -> usize { + panic!("not implemented"); + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + panic!("not implemented") + } + + fn get_device_info(&self) -> U2FDeviceInfo { + panic!("not implemented") + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + panic!("not implemented") + } +} + +impl HIDDevice for Device { + type BuildParameters = PathBuf; + type Id = PathBuf; + + fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> { + unimplemented!(); + } + + fn initialized(&self) -> bool { + unimplemented!(); + } + + fn id(&self) -> Self::Id { + unimplemented!() + } + + fn is_u2f(&mut self) -> bool { + unimplemented!() + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + unimplemented!() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + unimplemented!() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + unimplemented!() + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + unimplemented!() + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/stub/mod.rs b/third_party/rust/authenticator/src/transport/stub/mod.rs new file mode 100644 index 0000000000..0fab62d495 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/stub/mod.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// No-op module to permit compiling token HID support for Android, where +// no results are returned. + +#![allow(unused_variables)] + +pub mod device; +pub mod transaction; diff --git a/third_party/rust/authenticator/src/transport/stub/transaction.rs b/third_party/rust/authenticator/src/transport/stub/transaction.rs new file mode 100644 index 0000000000..d471c94da8 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/stub/transaction.rs @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use std::path::PathBuf; +use std::sync::mpsc::Sender; + +pub struct Transaction {} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + _status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + // Just to silence "unused"-warnings + let mut device_selector = DeviceSelector::run(); + let _ = DeviceSelectorEvent::DevicesAdded(vec![]); + let _ = DeviceSelectorEvent::DeviceRemoved(PathBuf::new()); + let _ = device_selector.clone_sender(); + device_selector.stop(); + + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotSupported, + ))); + + Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotSupported, + )) + } + + pub fn cancel(&mut self) { + /* No-op. */ + } +} diff --git a/third_party/rust/authenticator/src/transport/windows/device.rs b/third_party/rust/authenticator/src/transport/windows/device.rs new file mode 100644 index 0000000000..1037c25a20 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/device.rs @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use super::winapi::DeviceCapabilities; +use crate::consts::{CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::{FidoDevice, HIDError, SharedSecret}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use std::fs::{File, OpenOptions}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::os::windows::io::AsRawHandle; + +#[derive(Debug)] +pub struct Device { + path: String, + file: File, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> { + // Windows always includes the report ID. + let mut input = [0u8; MAX_HID_RPT_SIZE + 1]; + let _ = self.file.read(&mut input)?; + bytes.clone_from_slice(&input[1..]); + Ok(bytes.len()) + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + self.file.write(bytes) + } + + fn flush(&mut self) -> io::Result<()> { + self.file.flush() + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = String; + type Id = String; + + fn new(path: String) -> Result<Self, (HIDError, Self::Id)> { + debug!("Opening device {:?}", path); + let file = OpenOptions::new() + .read(true) + .write(true) + .open(&path) + .map_err(|e| (HIDError::IO(Some(path.clone().into()), e), path.clone()))?; + let mut res = Self { + path, + file, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path)) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + match DeviceCapabilities::new(self.file.as_raw_handle()) { + Ok(caps) => caps.usage() == FIDO_USAGE_U2FHID && caps.usage_page() == FIDO_USAGE_PAGE, + _ => false, + } + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/windows/mod.rs b/third_party/rust/authenticator/src/transport/windows/mod.rs new file mode 100644 index 0000000000..09135391dd --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod monitor; +mod winapi; diff --git a/third_party/rust/authenticator/src/transport/windows/monitor.rs b/third_party/rust/authenticator/src/transport/windows/monitor.rs new file mode 100644 index 0000000000..c73c012b05 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/monitor.rs @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::{DeviceID, DeviceSelectorEvent}; +use crate::transport::platform::winapi::DeviceInfoSet; +use runloop::RunLoop; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::iter::FromIterator; +use std::sync::{mpsc::Sender, Arc}; +use std::thread; +use std::time::Duration; + +pub struct Monitor<F> +where + F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap<String, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + let mut current = HashSet::new(); + let mut previous; + + while alive() { + let device_info_set = DeviceInfoSet::new()?; + previous = current; + current = HashSet::from_iter(device_info_set.devices()); + + // Remove devices that are gone. + for path in previous.difference(¤t) { + self.remove_device(path); + } + + let added: Vec<String> = current.difference(&previous).cloned().collect(); + + // We have to notify additions in batches to avoid + // arbitrarily selecting the first added device. + if !added.is_empty() + && self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(added.clone())) + .is_err() + { + // Send only fails if the receiver hung up. We should exit the loop. + break; + } + + // Add devices that were plugged in. + for path in added { + self.add_device(&path); + } + + // Wait a little before looking for devices again. + thread::sleep(Duration::from_millis(100)); + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn add_device(&mut self, path: &DeviceID) { + let f = self.new_device_cb.clone(); + let path = path.clone(); + let key = path.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + debug!("Adding device {}", path); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: &DeviceID) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path); + if let Some(runloop) = self.runloops.remove(path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(&path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/windows/transaction.rs b/third_party/rust/authenticator/src/transport/windows/transaction.rs new file mode 100644 index 0000000000..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/transaction.rs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/windows/winapi.rs b/third_party/rust/authenticator/src/transport/windows/winapi.rs new file mode 100644 index 0000000000..44b4489811 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/winapi.rs @@ -0,0 +1,263 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::io; +use std::mem; +use std::ptr; +use std::slice; + +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; + +use crate::util::io_err; + +extern crate libc; +extern crate winapi; + +use winapi::shared::{guiddef, minwindef, ntdef, windef}; +use winapi::shared::{hidclass, hidpi, hidusage}; +use winapi::um::{handleapi, setupapi}; + +#[link(name = "setupapi")] +extern "system" { + fn SetupDiGetClassDevsW( + ClassGuid: *const guiddef::GUID, + Enumerator: ntdef::PCSTR, + hwndParent: windef::HWND, + flags: minwindef::DWORD, + ) -> setupapi::HDEVINFO; + + fn SetupDiDestroyDeviceInfoList(DeviceInfoSet: setupapi::HDEVINFO) -> minwindef::BOOL; + + fn SetupDiEnumDeviceInterfaces( + DeviceInfoSet: setupapi::HDEVINFO, + DeviceInfoData: setupapi::PSP_DEVINFO_DATA, + InterfaceClassGuid: *const guiddef::GUID, + MemberIndex: minwindef::DWORD, + DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA, + ) -> minwindef::BOOL; + + fn SetupDiGetDeviceInterfaceDetailW( + DeviceInfoSet: setupapi::HDEVINFO, + DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA, + DeviceInterfaceDetailData: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W, + DeviceInterfaceDetailDataSize: minwindef::DWORD, + RequiredSize: minwindef::PDWORD, + DeviceInfoData: setupapi::PSP_DEVINFO_DATA, + ) -> minwindef::BOOL; +} + +#[link(name = "hid")] +extern "system" { + fn HidD_GetPreparsedData( + HidDeviceObject: ntdef::HANDLE, + PreparsedData: *mut hidpi::PHIDP_PREPARSED_DATA, + ) -> ntdef::BOOLEAN; + + fn HidD_FreePreparsedData(PreparsedData: hidpi::PHIDP_PREPARSED_DATA) -> ntdef::BOOLEAN; + + fn HidP_GetCaps( + PreparsedData: hidpi::PHIDP_PREPARSED_DATA, + Capabilities: hidpi::PHIDP_CAPS, + ) -> ntdef::NTSTATUS; +} + +fn from_wide_ptr(ptr: *const u16, len: usize) -> String { + assert!(!ptr.is_null() && len % 2 == 0); + let slice = unsafe { slice::from_raw_parts(ptr, len / 2) }; + OsString::from_wide(slice).to_string_lossy().into_owned() +} + +pub struct DeviceInfoSet { + set: setupapi::HDEVINFO, +} + +impl DeviceInfoSet { + pub fn new() -> io::Result<Self> { + let flags = setupapi::DIGCF_PRESENT | setupapi::DIGCF_DEVICEINTERFACE; + let set = unsafe { + SetupDiGetClassDevsW( + &hidclass::GUID_DEVINTERFACE_HID, + ptr::null_mut(), + ptr::null_mut(), + flags, + ) + }; + if set == handleapi::INVALID_HANDLE_VALUE { + return Err(io_err("SetupDiGetClassDevsW failed!")); + } + + Ok(Self { set }) + } + + pub fn get(&self) -> setupapi::HDEVINFO { + self.set + } + + pub fn devices(&self) -> DeviceInfoSetIter { + DeviceInfoSetIter::new(self) + } +} + +impl Drop for DeviceInfoSet { + fn drop(&mut self) { + let _ = unsafe { SetupDiDestroyDeviceInfoList(self.set) }; + } +} + +pub struct DeviceInfoSetIter<'a> { + set: &'a DeviceInfoSet, + index: minwindef::DWORD, +} + +impl<'a> DeviceInfoSetIter<'a> { + fn new(set: &'a DeviceInfoSet) -> Self { + Self { set, index: 0 } + } +} + +impl<'a> Iterator for DeviceInfoSetIter<'a> { + type Item = String; + + fn next(&mut self) -> Option<Self::Item> { + let mut device_interface_data = + mem::MaybeUninit::<setupapi::SP_DEVICE_INTERFACE_DATA>::zeroed(); + unsafe { + (*device_interface_data.as_mut_ptr()).cbSize = + mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DATA>() as minwindef::UINT; + } + + let rv = unsafe { + SetupDiEnumDeviceInterfaces( + self.set.get(), + ptr::null_mut(), + &hidclass::GUID_DEVINTERFACE_HID, + self.index, + device_interface_data.as_mut_ptr(), + ) + }; + if rv == 0 { + return None; // We're past the last device index. + } + + // Determine the size required to hold a detail struct. + let mut required_size = 0; + unsafe { + SetupDiGetDeviceInterfaceDetailW( + self.set.get(), + device_interface_data.as_mut_ptr(), + ptr::null_mut(), + required_size, + &mut required_size, + ptr::null_mut(), + ) + }; + if required_size == 0 { + return None; // An error occurred. + } + + let detail = DeviceInterfaceDetailData::new(required_size as usize)?; + let rv = unsafe { + SetupDiGetDeviceInterfaceDetailW( + self.set.get(), + device_interface_data.as_mut_ptr(), + detail.get(), + required_size, + ptr::null_mut(), + ptr::null_mut(), + ) + }; + if rv == 0 { + return None; // An error occurred. + } + + self.index += 1; + Some(detail.path()) + } +} + +struct DeviceInterfaceDetailData { + data: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W, + path_len: usize, +} + +impl DeviceInterfaceDetailData { + fn new(size: usize) -> Option<Self> { + let mut cb_size = mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W>(); + if cfg!(target_pointer_width = "32") { + cb_size = 4 + 2; // 4-byte uint + default TCHAR size. size_of is inaccurate. + } + + if size < cb_size { + warn!("DeviceInterfaceDetailData is too small. {}", size); + return None; + } + + let data = unsafe { libc::malloc(size) as setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W }; + if data.is_null() { + return None; + } + + // Set total size of the structure. + unsafe { (*data).cbSize = cb_size as minwindef::UINT }; + + // Compute offset of `SP_DEVICE_INTERFACE_DETAIL_DATA_W.DevicePath`. + let offset = memoffset::offset_of!(setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W, DevicePath); + + Some(Self { + data, + path_len: size - offset, + }) + } + + fn get(&self) -> setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W { + self.data + } + + fn path(&self) -> String { + unsafe { from_wide_ptr(ptr::addr_of!((*self.data).DevicePath[0]), self.path_len - 2) } + } +} + +impl Drop for DeviceInterfaceDetailData { + fn drop(&mut self) { + unsafe { libc::free(self.data as *mut libc::c_void) }; + } +} + +pub struct DeviceCapabilities { + caps: hidpi::HIDP_CAPS, +} + +impl DeviceCapabilities { + pub fn new(handle: ntdef::HANDLE) -> io::Result<Self> { + let mut preparsed_data = ptr::null_mut(); + let rv = unsafe { HidD_GetPreparsedData(handle, &mut preparsed_data) }; + if rv == 0 || preparsed_data.is_null() { + return Err(io_err("HidD_GetPreparsedData failed!")); + } + + let mut caps = mem::MaybeUninit::<hidpi::HIDP_CAPS>::uninit(); + unsafe { + let rv = HidP_GetCaps(preparsed_data, caps.as_mut_ptr()); + HidD_FreePreparsedData(preparsed_data); + + if rv != hidpi::HIDP_STATUS_SUCCESS { + return Err(io_err("HidP_GetCaps failed!")); + } + + Ok(Self { + caps: caps.assume_init(), + }) + } + } + + pub fn usage(&self) -> hidusage::USAGE { + self.caps.Usage + } + + pub fn usage_page(&self) -> hidusage::USAGE { + self.caps.UsagePage + } +} |