diff options
Diffstat (limited to 'third_party/rust/authenticator/src')
65 files changed, 7489 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/src/authenticatorservice.rs b/third_party/rust/authenticator/src/authenticatorservice.rs new file mode 100644 index 0000000000..fcb05dc63c --- /dev/null +++ b/third_party/rust/authenticator/src/authenticatorservice.rs @@ -0,0 +1,635 @@ +/* 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::sync::{mpsc::Sender, Arc, Mutex}; + +use crate::consts::PARAMETER_SIZE; +use crate::errors::*; +use crate::statecallback::StateCallback; + +pub trait AuthenticatorTransport { + /// The implementation of this method must return quickly and should + /// report its status via the status and callback methods + fn register( + &mut self, + flags: crate::RegisterFlags, + timeout: u64, + challenge: Vec<u8>, + application: crate::AppId, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()>; + + /// The implementation of this method must return quickly and should + /// report its status via the status and callback methods + fn sign( + &mut self, + flags: crate::SignFlags, + timeout: u64, + challenge: Vec<u8>, + app_ids: Vec<crate::AppId>, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()>; + + fn cancel(&mut self) -> crate::Result<()>; +} + +pub struct AuthenticatorService { + transports: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>, +} + +fn clone_and_configure_cancellation_callback<T>( + mut callback: StateCallback<T>, + transports_to_cancel: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>, +) -> StateCallback<T> { + callback.add_uncloneable_observer(Box::new(move || { + debug!( + "Callback observer is running, cancelling \ + {} unchosen transports...", + transports_to_cancel.len() + ); + for transport_mutex in &transports_to_cancel { + if let Err(e) = transport_mutex.lock().unwrap().cancel() { + error!("Cancellation failed: {:?}", e); + } + } + })); + callback +} + +impl AuthenticatorService { + pub fn new() -> crate::Result<Self> { + Ok(Self { + transports: Vec::new(), + }) + } + + /// Add any detected platform transports + pub fn add_detected_transports(&mut self) { + self.add_u2f_usb_hid_platform_transports(); + } + + fn add_transport(&mut self, boxed_token: Box<dyn AuthenticatorTransport + Send>) { + self.transports.push(Arc::new(Mutex::new(boxed_token))) + } + + pub fn add_u2f_usb_hid_platform_transports(&mut self) { + match crate::U2FManager::new() { + Ok(token) => self.add_transport(Box::new(token)), + Err(e) => error!("Could not add U2F HID transport: {}", e), + } + } + + #[cfg(feature = "webdriver")] + pub fn add_webdriver_virtual_bus(&mut self) { + match crate::virtualdevices::webdriver::VirtualManager::new() { + Ok(token) => { + println!("WebDriver ready, listening at {}", &token.url()); + self.add_transport(Box::new(token)); + } + Err(e) => error!("Could not add WebDriver virtual bus: {}", e), + } + } + + pub fn register( + &mut self, + flags: crate::RegisterFlags, + timeout: u64, + challenge: Vec<u8>, + application: crate::AppId, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for key_handle in &key_handles { + if key_handle.credential.len() > 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + debug!( + "register called with {} transports, iterable is {}", + self.transports.len(), + iterable_transports.len() + ); + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + debug!( + "register transports_to_cancel {}", + transports_to_cancel.len() + ); + + transport_mutex.lock().unwrap().register( + flags, + timeout, + challenge.clone(), + application.clone(), + key_handles.clone(), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + pub fn sign( + &mut self, + flags: crate::SignFlags, + timeout: u64, + challenge: Vec<u8>, + app_ids: Vec<crate::AppId>, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + if challenge.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + if app_ids.is_empty() { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for app_id in &app_ids { + if app_id.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + for key_handle in &key_handles { + if key_handle.credential.len() > 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + transport_mutex.lock().unwrap().sign( + flags, + timeout, + challenge.clone(), + app_ids.clone(), + key_handles.clone(), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + pub fn cancel(&mut self) -> crate::Result<()> { + if self.transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + for transport_mutex in &mut self.transports { + transport_mutex.lock().unwrap().cancel()?; + } + + Ok(()) + } +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::{AuthenticatorService, AuthenticatorTransport}; + use crate::consts::PARAMETER_SIZE; + use crate::statecallback::StateCallback; + use crate::{AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate}; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::mpsc::{channel, Sender}; + use std::sync::Arc; + use std::{io, thread}; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + pub struct TestTransportDriver { + consent: bool, + was_cancelled: Arc<AtomicBool>, + } + + impl TestTransportDriver { + pub fn new(consent: bool) -> io::Result<Self> { + Ok(Self { + consent, + was_cancelled: Arc::new(AtomicBool::new(false)), + }) + } + } + + impl TestTransportDriver { + fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo { + crate::u2ftypes::U2FDeviceInfo { + vendor_name: String::from("Mozilla").into_bytes(), + device_name: String::from("Test Transport Token").into_bytes(), + version_interface: 0, + version_major: 1, + version_minor: 2, + version_build: 3, + cap_flags: 0, + } + } + } + + impl AuthenticatorTransport for TestTransportDriver { + fn register( + &mut self, + _flags: crate::RegisterFlags, + _timeout: u64, + _challenge: Vec<u8>, + _application: crate::AppId, + _key_handles: Vec<crate::KeyHandle>, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + if self.consent { + let rv = Ok((vec![0u8; 16], self.dev_info())); + thread::spawn(move || callback.call(rv)); + } + Ok(()) + } + + fn sign( + &mut self, + _flags: crate::SignFlags, + _timeout: u64, + _challenge: Vec<u8>, + _app_ids: Vec<crate::AppId>, + _key_handles: Vec<crate::KeyHandle>, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + if self.consent { + let rv = Ok((vec![0u8; 0], vec![0u8; 0], vec![0u8; 0], self.dev_info())); + thread::spawn(move || callback.call(rv)); + } + Ok(()) + } + + fn cancel(&mut self) -> crate::Result<()> { + self.was_cancelled + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .map_or( + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::InvalidState, + )), + |_| Ok(()), + ) + } + } + + fn mk_key() -> KeyHandle { + KeyHandle { + credential: vec![0], + transports: AuthenticatorTransports::USB, + } + } + + fn mk_challenge() -> Vec<u8> { + vec![0x11; PARAMETER_SIZE] + } + + fn mk_appid() -> Vec<u8> { + vec![0x22; PARAMETER_SIZE] + } + + #[test] + fn test_no_challenge() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new().unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + RegisterFlags::empty(), + 1_000, + vec![], + mk_appid(), + vec![mk_key()], + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + + assert_matches!( + s.sign( + SignFlags::empty(), + 1_000, + vec![], + vec![mk_appid()], + vec![mk_key()], + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + } + + #[test] + fn test_no_appids() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new().unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + RegisterFlags::empty(), + 1_000, + mk_challenge(), + vec![], + vec![mk_key()], + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + + assert_matches!( + s.sign( + SignFlags::empty(), + 1_000, + mk_challenge(), + vec![], + vec![mk_key()], + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + } + + #[test] + fn test_no_keys() { + init(); + // No Keys is a resident-key use case. For U2F this would time out, + // but the actual reactions are up to the service implementation. + // This test yields OKs. + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new().unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + RegisterFlags::empty(), + 100, + mk_challenge(), + mk_appid(), + vec![], + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ), + Ok(()) + ); + + assert_matches!( + s.sign( + SignFlags::empty(), + 100, + mk_challenge(), + vec![mk_appid()], + vec![], + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ), + Ok(()) + ); + } + + #[test] + fn test_large_keys() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let large_key = KeyHandle { + credential: vec![0; 257], + transports: AuthenticatorTransports::USB, + }; + + let mut s = AuthenticatorService::new().unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + RegisterFlags::empty(), + 1_000, + mk_challenge(), + mk_appid(), + vec![large_key.clone()], + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + + assert_matches!( + s.sign( + SignFlags::empty(), + 1_000, + mk_challenge(), + vec![mk_appid()], + vec![large_key], + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + } + + #[test] + fn test_no_transports() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new().unwrap(); + assert_matches!( + s.register( + RegisterFlags::empty(), + 1_000, + mk_challenge(), + mk_appid(), + vec![mk_key()], + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::NoConfiguredTransports + ); + + assert_matches!( + s.sign( + SignFlags::empty(), + 1_000, + mk_challenge(), + vec![mk_appid()], + vec![mk_key()], + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::NoConfiguredTransports + ); + + assert_matches!( + s.cancel().unwrap_err(), + crate::errors::AuthenticatorError::NoConfiguredTransports + ); + } + + #[test] + fn test_cancellation_register() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new().unwrap(); + let ttd_one = TestTransportDriver::new(true).unwrap(); + let ttd_two = TestTransportDriver::new(false).unwrap(); + let ttd_three = TestTransportDriver::new(false).unwrap(); + + let was_cancelled_one = ttd_one.was_cancelled.clone(); + let was_cancelled_two = ttd_two.was_cancelled.clone(); + let was_cancelled_three = ttd_three.was_cancelled.clone(); + + s.add_transport(Box::new(ttd_one)); + s.add_transport(Box::new(ttd_two)); + s.add_transport(Box::new(ttd_three)); + + let callback = StateCallback::new(Box::new(move |_rv| {})); + assert!(s + .register( + RegisterFlags::empty(), + 1_000, + mk_challenge(), + mk_appid(), + vec![], + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + assert_eq!(was_cancelled_one.load(Ordering::SeqCst), false); + assert_eq!(was_cancelled_two.load(Ordering::SeqCst), true); + assert_eq!(was_cancelled_three.load(Ordering::SeqCst), true); + } + + #[test] + fn test_cancellation_sign() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new().unwrap(); + let ttd_one = TestTransportDriver::new(true).unwrap(); + let ttd_two = TestTransportDriver::new(false).unwrap(); + let ttd_three = TestTransportDriver::new(false).unwrap(); + + let was_cancelled_one = ttd_one.was_cancelled.clone(); + let was_cancelled_two = ttd_two.was_cancelled.clone(); + let was_cancelled_three = ttd_three.was_cancelled.clone(); + + s.add_transport(Box::new(ttd_one)); + s.add_transport(Box::new(ttd_two)); + s.add_transport(Box::new(ttd_three)); + + let callback = StateCallback::new(Box::new(move |_rv| {})); + assert!(s + .sign( + SignFlags::empty(), + 1_000, + mk_challenge(), + vec![mk_appid()], + vec![mk_key()], + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + assert_eq!(was_cancelled_one.load(Ordering::SeqCst), false); + assert_eq!(was_cancelled_two.load(Ordering::SeqCst), true); + assert_eq!(was_cancelled_three.load(Ordering::SeqCst), true); + } + + #[test] + fn test_cancellation_race() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new().unwrap(); + // Let both of these race which one provides consent. + let ttd_one = TestTransportDriver::new(true).unwrap(); + let ttd_two = TestTransportDriver::new(true).unwrap(); + + let was_cancelled_one = ttd_one.was_cancelled.clone(); + let was_cancelled_two = ttd_two.was_cancelled.clone(); + + s.add_transport(Box::new(ttd_one)); + s.add_transport(Box::new(ttd_two)); + + let callback = StateCallback::new(Box::new(move |_rv| {})); + assert!(s + .register( + RegisterFlags::empty(), + 1_000, + mk_challenge(), + mk_appid(), + vec![], + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + let one = was_cancelled_one.load(Ordering::SeqCst); + let two = was_cancelled_two.load(Ordering::SeqCst); + assert_eq!( + one ^ two, + true, + "asserting that one={} xor two={} is true", + one, + two + ); + } +} diff --git a/third_party/rust/authenticator/src/capi.rs b/third_party/rust/authenticator/src/capi.rs new file mode 100644 index 0000000000..ea7123503f --- /dev/null +++ b/third_party/rust/authenticator/src/capi.rs @@ -0,0 +1,377 @@ +/* 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::authenticatorservice::AuthenticatorService; +use crate::errors; +use crate::statecallback::StateCallback; +use crate::{RegisterResult, SignResult}; +use libc::size_t; +use rand::{thread_rng, Rng}; +use std::collections::HashMap; +use std::sync::mpsc::channel; +use std::thread; +use std::{ptr, slice}; + +type U2FAppIds = Vec<crate::AppId>; +type U2FKeyHandles = Vec<crate::KeyHandle>; +type U2FCallback = extern "C" fn(u64, *mut U2FResult); + +pub enum U2FResult { + Success(HashMap<u8, Vec<u8>>), + Error(errors::AuthenticatorError), +} + +const RESBUF_ID_REGISTRATION: u8 = 0; +const RESBUF_ID_KEYHANDLE: u8 = 1; +const RESBUF_ID_SIGNATURE: u8 = 2; +const RESBUF_ID_APPID: u8 = 3; +const RESBUF_ID_VENDOR_NAME: u8 = 4; +const RESBUF_ID_DEVICE_NAME: u8 = 5; +const RESBUF_ID_FIRMWARE_MAJOR: u8 = 6; +const RESBUF_ID_FIRMWARE_MINOR: u8 = 7; +const RESBUF_ID_FIRMWARE_BUILD: u8 = 8; + +// Generates a new 64-bit transaction id with collision probability 2^-32. +fn new_tid() -> u64 { + thread_rng().gen::<u64>() +} + +unsafe fn from_raw(ptr: *const u8, len: usize) -> Vec<u8> { + slice::from_raw_parts(ptr, len).to_vec() +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +#[no_mangle] +pub extern "C" fn rust_u2f_mgr_new() -> *mut AuthenticatorService { + if let Ok(mut mgr) = AuthenticatorService::new() { + mgr.add_detected_transports(); + Box::into_raw(Box::new(mgr)) + } else { + ptr::null_mut() + } +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_free(mgr: *mut AuthenticatorService) { + if !mgr.is_null() { + Box::from_raw(mgr); + } +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_new() -> *mut U2FAppIds { + Box::into_raw(Box::new(vec![])) +} + +/// # Safety +/// +/// This method must be used on an actual U2FAppIds handle +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_add( + ids: *mut U2FAppIds, + id_ptr: *const u8, + id_len: usize, +) { + (*ids).push(from_raw(id_ptr, id_len)); +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_free(ids: *mut U2FAppIds) { + if !ids.is_null() { + Box::from_raw(ids); + } +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_khs_new() -> *mut U2FKeyHandles { + Box::into_raw(Box::new(vec![])) +} + +/// # Safety +/// +/// This method must be used on an actual U2FKeyHandles handle +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_khs_add( + khs: *mut U2FKeyHandles, + key_handle_ptr: *const u8, + key_handle_len: usize, + transports: u8, +) { + (*khs).push(crate::KeyHandle { + credential: from_raw(key_handle_ptr, key_handle_len), + transports: crate::AuthenticatorTransports::from_bits_truncate(transports), + }); +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_khs_free(khs: *mut U2FKeyHandles) { + if !khs.is_null() { + Box::from_raw(khs); + } +} + +/// # Safety +/// +/// This method must be used on an actual U2FResult handle +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_result_error(res: *const U2FResult) -> u8 { + if res.is_null() { + return errors::U2FTokenError::Unknown as u8; + } + + if let U2FResult::Error(ref err) = *res { + return err.as_u2f_errorcode(); + } + + 0 /* No error, the request succeeded. */ +} + +/// # Safety +/// +/// This method must be used before rust_u2f_resbuf_copy +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_resbuf_length( + res: *const U2FResult, + bid: u8, + len: *mut size_t, +) -> bool { + if res.is_null() { + return false; + } + + if let U2FResult::Success(ref bufs) = *res { + if let Some(buf) = bufs.get(&bid) { + *len = buf.len(); + return true; + } + } + + false +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_u2f_resbuf_length) +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_resbuf_copy( + res: *const U2FResult, + bid: u8, + dst: *mut u8, +) -> bool { + if res.is_null() { + return false; + } + + if let U2FResult::Success(ref bufs) = *res { + if let Some(buf) = bufs.get(&bid) { + ptr::copy_nonoverlapping(buf.as_ptr(), dst, buf.len()); + return true; + } + } + + false +} + +/// # Safety +/// +/// This method should not be called on U2FResult handles after they've been +/// freed or a double-free will occur +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_res_free(res: *mut U2FResult) { + if !res.is_null() { + Box::from_raw(res); + } +} + +/// # Safety +/// +/// This method should not be called on AuthenticatorService handles after +/// they've been freed +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_register( + mgr: *mut AuthenticatorService, + flags: u64, + timeout: u64, + callback: U2FCallback, + challenge_ptr: *const u8, + challenge_len: usize, + application_ptr: *const u8, + application_len: usize, + khs: *const U2FKeyHandles, +) -> u64 { + if mgr.is_null() { + return 0; + } + + // Check buffers. + if challenge_ptr.is_null() || application_ptr.is_null() { + return 0; + } + + let flags = crate::RegisterFlags::from_bits_truncate(flags); + let challenge = from_raw(challenge_ptr, challenge_len); + let application = from_raw(application_ptr, application_len); + let key_handles = (*khs).clone(); + + let (status_tx, status_rx) = channel::<crate::StatusUpdate>(); + thread::spawn(move || loop { + // Issue https://github.com/mozilla/authenticator-rs/issues/132 will + // plumb the status channel through to the actual C API signatures + match status_rx.recv() { + Ok(_) => {} + Err(_recv_error) => return, + } + }); + + let tid = new_tid(); + + let state_callback = StateCallback::<crate::Result<RegisterResult>>::new(Box::new(move |rv| { + let result = match rv { + Ok((registration, dev_info)) => { + let mut bufs = HashMap::new(); + bufs.insert(RESBUF_ID_REGISTRATION, registration); + bufs.insert(RESBUF_ID_VENDOR_NAME, dev_info.vendor_name); + bufs.insert(RESBUF_ID_DEVICE_NAME, dev_info.device_name); + bufs.insert(RESBUF_ID_FIRMWARE_MAJOR, vec![dev_info.version_major]); + bufs.insert(RESBUF_ID_FIRMWARE_MINOR, vec![dev_info.version_minor]); + bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]); + U2FResult::Success(bufs) + } + Err(e) => U2FResult::Error(e), + }; + + callback(tid, Box::into_raw(Box::new(result))); + })); + + let res = (*mgr).register( + flags, + timeout, + challenge, + application, + key_handles, + status_tx, + state_callback, + ); + + if res.is_ok() { + tid + } else { + 0 + } +} + +/// # Safety +/// +/// This method should not be called on AuthenticatorService handles after +/// they've been freed +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_sign( + mgr: *mut AuthenticatorService, + flags: u64, + timeout: u64, + callback: U2FCallback, + challenge_ptr: *const u8, + challenge_len: usize, + app_ids: *const U2FAppIds, + khs: *const U2FKeyHandles, +) -> u64 { + if mgr.is_null() || khs.is_null() { + return 0; + } + + // Check buffers. + if challenge_ptr.is_null() { + return 0; + } + + // Need at least one app_id. + if (*app_ids).is_empty() { + return 0; + } + + let flags = crate::SignFlags::from_bits_truncate(flags); + let challenge = from_raw(challenge_ptr, challenge_len); + let app_ids = (*app_ids).clone(); + let key_handles = (*khs).clone(); + + let (status_tx, status_rx) = channel::<crate::StatusUpdate>(); + thread::spawn(move || loop { + // Issue https://github.com/mozilla/authenticator-rs/issues/132 will + // plumb the status channel through to the actual C API signatures + match status_rx.recv() { + Ok(_) => {} + Err(_recv_error) => return, + } + }); + + let tid = new_tid(); + let state_callback = StateCallback::<crate::Result<SignResult>>::new(Box::new(move |rv| { + let result = match rv { + Ok((app_id, key_handle, signature, dev_info)) => { + let mut bufs = HashMap::new(); + bufs.insert(RESBUF_ID_KEYHANDLE, key_handle); + bufs.insert(RESBUF_ID_SIGNATURE, signature); + bufs.insert(RESBUF_ID_APPID, app_id); + bufs.insert(RESBUF_ID_VENDOR_NAME, dev_info.vendor_name); + bufs.insert(RESBUF_ID_DEVICE_NAME, dev_info.device_name); + bufs.insert(RESBUF_ID_FIRMWARE_MAJOR, vec![dev_info.version_major]); + bufs.insert(RESBUF_ID_FIRMWARE_MINOR, vec![dev_info.version_minor]); + bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]); + U2FResult::Success(bufs) + } + Err(e) => U2FResult::Error(e), + }; + + callback(tid, Box::into_raw(Box::new(result))); + })); + + let res = (*mgr).sign( + flags, + timeout, + challenge, + app_ids, + key_handles, + status_tx, + state_callback, + ); + + if res.is_ok() { + tid + } else { + 0 + } +} + +/// # Safety +/// +/// This method should not be called AuthenticatorService handles after they've +/// been freed +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_cancel(mgr: *mut AuthenticatorService) { + if !mgr.is_null() { + // Ignore return value. + let _ = (*mgr).cancel(); + } +} diff --git a/third_party/rust/authenticator/src/consts.rs b/third_party/rust/authenticator/src/consts.rs new file mode 100644 index 0000000000..5f27bb9378 --- /dev/null +++ b/third_party/rust/authenticator/src/consts.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/. */ + +// Allow dead code in this module, since it's all packet consts anyways. +#![allow(dead_code)] + +pub const MAX_HID_RPT_SIZE: usize = 64; +pub const U2FAPDUHEADER_SIZE: usize = 7; +pub const CID_BROADCAST: [u8; 4] = [0xff, 0xff, 0xff, 0xff]; +pub const TYPE_MASK: u8 = 0x80; +pub const TYPE_INIT: u8 = 0x80; +pub const TYPE_CONT: u8 = 0x80; + +// Size of header in U2F Init USB HID Packets +pub const INIT_HEADER_SIZE: usize = 7; +// Size of header in U2F Cont USB HID Packets +pub const CONT_HEADER_SIZE: usize = 5; + +pub const PARAMETER_SIZE: usize = 32; + +pub const FIDO_USAGE_PAGE: u16 = 0xf1d0; // FIDO alliance HID usage page +pub const FIDO_USAGE_U2FHID: u16 = 0x01; // U2FHID usage for top-level collection +pub const FIDO_USAGE_DATA_IN: u8 = 0x20; // Raw IN data report +pub const FIDO_USAGE_DATA_OUT: u8 = 0x21; // Raw OUT data report + +// General pub constants + +pub const U2FHID_IF_VERSION: u32 = 2; // Current interface implementation version +pub const U2FHID_FRAME_TIMEOUT: u32 = 500; // Default frame timeout in ms +pub const U2FHID_TRANS_TIMEOUT: u32 = 3000; // Default message timeout in ms + +// U2FHID native commands +pub const U2FHID_PING: u8 = TYPE_INIT | 0x01; // Echo data through local processor only +pub const U2FHID_MSG: u8 = TYPE_INIT | 0x03; // Send U2F message frame +pub const U2FHID_LOCK: u8 = TYPE_INIT | 0x04; // Send lock channel command +pub const U2FHID_INIT: u8 = TYPE_INIT | 0x06; // Channel initialization +pub const U2FHID_WINK: u8 = TYPE_INIT | 0x08; // Send device identification wink +pub const U2FHID_ERROR: u8 = TYPE_INIT | 0x3f; // Error response + +// U2FHID_MSG commands +pub const U2F_VENDOR_FIRST: u8 = TYPE_INIT | 0x40; // First vendor defined command +pub const U2F_VENDOR_LAST: u8 = TYPE_INIT | 0x7f; // Last vendor defined command +pub const U2F_REGISTER: u8 = 0x01; // Registration command +pub const U2F_AUTHENTICATE: u8 = 0x02; // Authenticate/sign command +pub const U2F_VERSION: u8 = 0x03; // Read version string command + +pub const YKPIV_INS_GET_VERSION: u8 = 0xfd; // Get firmware version, yubico ext + +// U2F_REGISTER command defines +pub const U2F_REGISTER_ID: u8 = 0x05; // Version 2 registration identifier +pub const U2F_REGISTER_HASH_ID: u8 = 0x00; // Version 2 hash identintifier + +// U2F_AUTHENTICATE command defines +pub const U2F_REQUEST_USER_PRESENCE: u8 = 0x03; // Verify user presence and sign +pub const U2F_CHECK_IS_REGISTERED: u8 = 0x07; // Check if the key handle is registered + +// U2FHID_INIT command defines +pub const INIT_NONCE_SIZE: usize = 8; // Size of channel initialization challenge +pub const CAPFLAG_WINK: u8 = 0x01; // Device supports WINK command +pub const CAPFLAG_LOCK: u8 = 0x02; // Device supports LOCK command + +// Low-level error codes. Return as negatives. + +pub const ERR_NONE: u8 = 0x00; // No error +pub const ERR_INVALID_CMD: u8 = 0x01; // Invalid command +pub const ERR_INVALID_PAR: u8 = 0x02; // Invalid parameter +pub const ERR_INVALID_LEN: u8 = 0x03; // Invalid message length +pub const ERR_INVALID_SEQ: u8 = 0x04; // Invalid message sequencing +pub const ERR_MSG_TIMEOUT: u8 = 0x05; // Message has timed out +pub const ERR_CHANNEL_BUSY: u8 = 0x06; // Channel busy +pub const ERR_LOCK_REQUIRED: u8 = 0x0a; // Command requires channel lock +pub const ERR_INVALID_CID: u8 = 0x0b; // Command not allowed on this cid +pub const ERR_OTHER: u8 = 0x7f; // Other unspecified error + +// These are ISO 7816-4 defined response status words. +pub const SW_NO_ERROR: [u8; 2] = [0x90, 0x00]; +pub const SW_CONDITIONS_NOT_SATISFIED: [u8; 2] = [0x69, 0x85]; +pub const SW_WRONG_DATA: [u8; 2] = [0x6A, 0x80]; +pub const SW_WRONG_LENGTH: [u8; 2] = [0x67, 0x00]; diff --git a/third_party/rust/authenticator/src/errors.rs b/third_party/rust/authenticator/src/errors.rs new file mode 100644 index 0000000000..ee63cfacf3 --- /dev/null +++ b/third_party/rust/authenticator/src/errors.rs @@ -0,0 +1,96 @@ +/* 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::fmt; +use std::io; +use std::sync::mpsc; + +// This composite error type is patterned from Phil Daniels' blog: +// https://www.philipdaniels.com/blog/2019/defining-rust-error-types/ + +#[derive(Debug)] +pub enum AuthenticatorError { + // Errors from external libraries... + Io(io::Error), + // Errors raised by us... + InvalidRelyingPartyInput, + NoConfiguredTransports, + Platform, + InternalError(String), + U2FToken(U2FTokenError), + Custom(String), +} + +impl AuthenticatorError { + pub fn as_u2f_errorcode(&self) -> u8 { + match *self { + AuthenticatorError::U2FToken(ref err) => *err as u8, + _ => U2FTokenError::Unknown as u8, + } + } +} + +impl std::error::Error for AuthenticatorError {} + +impl fmt::Display for AuthenticatorError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + AuthenticatorError::Io(ref err) => err.fmt(f), + AuthenticatorError::InvalidRelyingPartyInput => { + write!(f, "invalid input from relying party") + } + AuthenticatorError::NoConfiguredTransports => write!( + f, + "no transports were configured in the authenticator service" + ), + AuthenticatorError::Platform => write!(f, "unknown platform error"), + AuthenticatorError::InternalError(ref err) => write!(f, "internal error: {}", err), + AuthenticatorError::U2FToken(ref err) => { + write!(f, "A u2f token error occurred {:?}", err) + } + AuthenticatorError::Custom(ref err) => write!(f, "A custom error occurred {:?}", err), + } + } +} + +impl From<io::Error> for AuthenticatorError { + fn from(err: io::Error) -> AuthenticatorError { + AuthenticatorError::Io(err) + } +} + +impl<T> From<mpsc::SendError<T>> for AuthenticatorError { + fn from(err: mpsc::SendError<T>) -> AuthenticatorError { + AuthenticatorError::InternalError(err.to_string()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum U2FTokenError { + Unknown = 1, + NotSupported = 2, + InvalidState = 3, + ConstraintError = 4, + NotAllowed = 5, +} + +impl U2FTokenError { + fn as_str(&self) -> &str { + match *self { + U2FTokenError::Unknown => "unknown", + U2FTokenError::NotSupported => "not supported", + U2FTokenError::InvalidState => "invalid state", + U2FTokenError::ConstraintError => "constraint error", + U2FTokenError::NotAllowed => "not allowed", + } + } +} + +impl std::fmt::Display for U2FTokenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl std::error::Error for U2FTokenError {} diff --git a/third_party/rust/authenticator/src/freebsd/device.rs b/third_party/rust/authenticator/src/freebsd/device.rs new file mode 100644 index 0000000000..32069cd5f9 --- /dev/null +++ b/third_party/rust/authenticator/src/freebsd/device.rs @@ -0,0 +1,112 @@ +/* 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, OsString}; +use std::io; +use std::io::{Read, Write}; +use std::os::unix::prelude::*; + +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::platform::uhid; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::from_unix_result; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: libc::c_int, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, +} + +impl Device { + pub fn new(path: OsString) -> io::Result<Self> { + let cstr = CString::new(path.as_bytes())?; + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) }; + let fd = from_unix_result(fd)?; + Ok(Self { + path, + fd, + cid: CID_BROADCAST, + dev_info: None, + }) + } + + pub fn is_u2f(&self) -> bool { + uhid::is_u2f_device(self.fd) + } +} + +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 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<'a>(&'a self) -> &'a [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); + } +} diff --git a/third_party/rust/authenticator/src/freebsd/mod.rs b/third_party/rust/authenticator/src/freebsd/mod.rs new file mode 100644 index 0000000000..7ed5727157 --- /dev/null +++ b/third_party/rust/authenticator/src/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/freebsd/monitor.rs b/third_party/rust/authenticator/src/freebsd/monitor.rs new file mode 100644 index 0000000000..d9153e2ef2 --- /dev/null +++ b/third_party/rust/authenticator/src/freebsd/monitor.rs @@ -0,0 +1,133 @@ +/* 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 devd_rs; +use std::collections::HashMap; +use std::ffi::OsString; +use std::sync::Arc; +use std::{fs, io}; + +use runloop::RunLoop; + +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, &dyn Fn() -> bool) + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, +} + +impl<F> Monitor<F> +where + F: Fn(OsString, &dyn Fn() -> bool) + Send + Sync + 'static, +{ + pub fn new(new_device_cb: F) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> { + let mut ctx = devd_rs::Context::new().map_err(convert_error)?; + + // Iterate all existing devices. + for dev in fs::read_dir("/dev")? { + if let Ok(dev) = dev { + let filename_ = dev.file_name(); + let filename = filename_.to_str().unwrap_or(""); + if filename.starts_with("uhid") { + self.add_device(("/dev/".to_owned() + filename).into()); + } + } + } + + // 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(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) => { + 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 key = path.clone(); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: OsString) { + 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/freebsd/transaction.rs b/third_party/rust/authenticator/src/freebsd/transaction.rs new file mode 100644 index 0000000000..e7cd00f184 --- /dev/null +++ b/third_party/rust/authenticator/src/freebsd/transaction.rs @@ -0,0 +1,53 @@ +/* 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::platform::monitor::Monitor; +use crate::statecallback::StateCallback; +use runloop::RunLoop; +use std::ffi::OsString; + +pub struct Transaction { + // Handle to the thread loop. + thread: Option<RunLoop>, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn(OsString, &dyn Fn() -> bool) + Sync + Send + 'static, + T: 'static, + { + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb); + + // 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: Some(thread), + }) + } + + pub fn cancel(&mut self) { + // This must never be None. + self.thread.take().unwrap().cancel(); + } +} diff --git a/third_party/rust/authenticator/src/freebsd/uhid.rs b/third_party/rust/authenticator/src/freebsd/uhid.rs new file mode 100644 index 0000000000..deb197ea8f --- /dev/null +++ b/third_party/rust/authenticator/src/freebsd/uhid.rs @@ -0,0 +1,92 @@ +/* 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::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::with_capacity(desc.ugd_actlen as usize); + unsafe { + value.set_len(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/hidproto.rs b/third_party/rust/authenticator/src/hidproto.rs new file mode 100644 index 0000000000..5679e6e5d7 --- /dev/null +++ b/third_party/rust/authenticator/src/hidproto.rs @@ -0,0 +1,253 @@ +/* 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) +)] + +use std::io; +use std::mem; + +use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, 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 +} + +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 != None { + 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/lib.rs b/third_party/rust/authenticator/src/lib.rs new file mode 100644 index 0000000000..cfe82deb2b --- /dev/null +++ b/third_party/rust/authenticator/src/lib.rs @@ -0,0 +1,129 @@ +/* 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/. */ + +#[macro_use] +mod util; + +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +pub mod hidproto; + +#[cfg(any(target_os = "linux"))] +extern crate libudev; + +#[cfg(any(target_os = "linux"))] +#[path = "linux/mod.rs"] +pub mod platform; + +#[cfg(any(target_os = "freebsd"))] +extern crate devd_rs; + +#[cfg(any(target_os = "freebsd"))] +#[path = "freebsd/mod.rs"] +pub mod platform; + +#[cfg(any(target_os = "netbsd"))] +#[path = "netbsd/mod.rs"] +pub mod platform; + +#[cfg(any(target_os = "openbsd"))] +#[path = "openbsd/mod.rs"] +pub mod platform; + +#[cfg(any(target_os = "macos"))] +extern crate core_foundation; + +#[cfg(any(target_os = "macos"))] +#[path = "macos/mod.rs"] +pub mod platform; + +#[cfg(any(target_os = "windows"))] +#[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" +)))] +#[path = "stub/mod.rs"] +pub mod platform; + +extern crate libc; +#[macro_use] +extern crate log; +extern crate rand; +extern crate runloop; + +#[macro_use] +extern crate bitflags; + +pub mod authenticatorservice; +mod consts; +mod statemachine; +mod u2fprotocol; +mod u2ftypes; + +mod manager; +pub use crate::manager::U2FManager; + +mod capi; +pub use crate::capi::*; + +pub mod errors; +pub mod statecallback; +mod virtualdevices; + +// Keep this in sync with the constants in u2fhid-capi.h. +bitflags! { + pub struct RegisterFlags: u64 { + const REQUIRE_RESIDENT_KEY = 1; + const REQUIRE_USER_VERIFICATION = 2; + const REQUIRE_PLATFORM_ATTACHMENT = 4; + } +} +bitflags! { + pub struct SignFlags: u64 { + const REQUIRE_USER_VERIFICATION = 1; + } +} +bitflags! { + pub struct AuthenticatorTransports: u8 { + const USB = 1; + const NFC = 2; + const BLE = 4; + } +} + +#[derive(Clone)] +pub struct KeyHandle { + pub credential: Vec<u8>, + pub transports: AuthenticatorTransports, +} + +pub type AppId = Vec<u8>; +pub type RegisterResult = (Vec<u8>, u2ftypes::U2FDeviceInfo); +pub type SignResult = (AppId, Vec<u8>, Vec<u8>, u2ftypes::U2FDeviceInfo); + +pub type Result<T> = std::result::Result<T, errors::AuthenticatorError>; + +#[derive(Debug, Clone)] +pub enum StatusUpdate { + DeviceAvailable { dev_info: u2ftypes::U2FDeviceInfo }, + DeviceUnavailable { dev_info: u2ftypes::U2FDeviceInfo }, + Success { dev_info: u2ftypes::U2FDeviceInfo }, +} + +#[cfg(test)] +#[macro_use] +extern crate assert_matches; + +#[cfg(fuzzing)] +pub use consts::*; +#[cfg(fuzzing)] +pub use u2fprotocol::*; +#[cfg(fuzzing)] +pub use u2ftypes::*; diff --git a/third_party/rust/authenticator/src/linux/device.rs b/third_party/rust/authenticator/src/linux/device.rs new file mode 100644 index 0000000000..4a0d58d6d7 --- /dev/null +++ b/third_party/rust/authenticator/src/linux/device.rs @@ -0,0 +1,112 @@ +/* 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, OsString}; +use std::io; +use std::io::{Read, Write}; +use std::os::unix::prelude::*; + +use crate::consts::CID_BROADCAST; +use crate::platform::{hidraw, monitor}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::from_unix_result; + +#[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>, +} + +impl Device { + pub fn new(path: OsString) -> io::Result<Self> { + let cstr = CString::new(path.as_bytes())?; + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) }; + let fd = from_unix_result(fd)?; + let (in_rpt_size, out_rpt_size) = hidraw::read_hid_rpt_sizes_or_defaults(fd); + Ok(Self { + path, + fd, + in_rpt_size, + out_rpt_size, + cid: CID_BROADCAST, + dev_info: None, + }) + } + + pub fn is_u2f(&self) -> bool { + hidraw::is_u2f_device(self.fd) + } +} + +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 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 bufp = buf.as_ptr() as *const libc::c_void; + let rv = unsafe { libc::write(self.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); + } +} diff --git a/third_party/rust/authenticator/src/linux/hidraw.rs b/third_party/rust/authenticator/src/linux/hidraw.rs new file mode 100644 index 0000000000..662ea83298 --- /dev/null +++ b/third_party/rust/authenticator/src/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::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/linux/hidwrapper.h b/third_party/rust/authenticator/src/linux/hidwrapper.h new file mode 100644 index 0000000000..ce77e0f1ca --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/hidwrapper.rs b/third_party/rust/authenticator/src/linux/hidwrapper.rs new file mode 100644 index 0000000000..ea1a39051b --- /dev/null +++ b/third_party/rust/authenticator/src/linux/hidwrapper.rs @@ -0,0 +1,48 @@ +#![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"); diff --git a/third_party/rust/authenticator/src/linux/ioctl_aarch64le.rs b/third_party/rust/authenticator/src/linux/ioctl_aarch64le.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_armle.rs b/third_party/rust/authenticator/src/linux/ioctl_armle.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_mips64le.rs b/third_party/rust/authenticator/src/linux/ioctl_mips64le.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_mipsbe.rs b/third_party/rust/authenticator/src/linux/ioctl_mipsbe.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_mipsle.rs b/third_party/rust/authenticator/src/linux/ioctl_mipsle.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_powerpc64be.rs b/third_party/rust/authenticator/src/linux/ioctl_powerpc64be.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_powerpc64le.rs b/third_party/rust/authenticator/src/linux/ioctl_powerpc64le.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_powerpcbe.rs b/third_party/rust/authenticator/src/linux/ioctl_powerpcbe.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_s390xbe.rs b/third_party/rust/authenticator/src/linux/ioctl_s390xbe.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_x86.rs b/third_party/rust/authenticator/src/linux/ioctl_x86.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/ioctl_x86_64.rs b/third_party/rust/authenticator/src/linux/ioctl_x86_64.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/mod.rs b/third_party/rust/authenticator/src/linux/mod.rs new file mode 100644 index 0000000000..c4d490ecee --- /dev/null +++ b/third_party/rust/authenticator/src/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/linux/monitor.rs b/third_party/rust/authenticator/src/linux/monitor.rs new file mode 100644 index 0000000000..595e2f3ddf --- /dev/null +++ b/third_party/rust/authenticator/src/linux/monitor.rs @@ -0,0 +1,168 @@ +/* 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 libc::{c_int, c_short, c_ulong}; +use libudev::EventType; +use runloop::RunLoop; +use std::collections::HashMap; +use std::ffi::OsString; +use std::io; +use std::os::unix::io::AsRawFd; +use std::sync::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((&mut 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(OsString, &dyn Fn() -> bool) + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, +} + +impl<F> Monitor<F> +where + F: Fn(OsString, &dyn Fn() -> bool) + Send + Sync + 'static, +{ + pub fn new(new_device_cb: F) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> { + let ctx = libudev::Context::new()?; + + let mut enumerator = libudev::Enumerator::new(&ctx)?; + enumerator.match_subsystem(UDEV_SUBSYSTEM)?; + + // Iterate all existing devices. + for dev in enumerator.scan_devices()? { + if let Some(path) = dev.devnode().map(|p| p.to_owned().into_os_string()) { + 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().into_os_string()); + + match (event.event_type(), path) { + (EventType::Add, Some(path)) => { + self.add_device(path); + } + (EventType::Remove, Some(path)) => { + self.remove_device(&path); + } + _ => { /* ignore other types and failures */ } + } + } + + fn add_device(&mut self, path: OsString) { + let f = self.new_device_cb.clone(); + let key = path.clone(); + + debug!("Adding device {}", path.to_string_lossy()); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: &OsString) { + 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: &OsString, 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/linux/transaction.rs b/third_party/rust/authenticator/src/linux/transaction.rs new file mode 100644 index 0000000000..e7cd00f184 --- /dev/null +++ b/third_party/rust/authenticator/src/linux/transaction.rs @@ -0,0 +1,53 @@ +/* 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::platform::monitor::Monitor; +use crate::statecallback::StateCallback; +use runloop::RunLoop; +use std::ffi::OsString; + +pub struct Transaction { + // Handle to the thread loop. + thread: Option<RunLoop>, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn(OsString, &dyn Fn() -> bool) + Sync + Send + 'static, + T: 'static, + { + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb); + + // 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: Some(thread), + }) + } + + pub fn cancel(&mut self) { + // This must never be None. + self.thread.take().unwrap().cancel(); + } +} diff --git a/third_party/rust/authenticator/src/macos/device.rs b/third_party/rust/authenticator/src/macos/device.rs new file mode 100644 index 0000000000..425a27959b --- /dev/null +++ b/third_party/rust/authenticator/src/macos/device.rs @@ -0,0 +1,156 @@ +/* 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::platform::iokit::*; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use core_foundation::base::*; +use core_foundation::string::*; +use std::convert::TryInto; +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: Receiver<Vec<u8>>, + dev_info: Option<U2FDeviceInfo>, +} + +impl Device { + pub fn new(dev_ids: (IOHIDDeviceRef, Receiver<Vec<u8>>)) -> io::Result<Self> { + let (device_ref, report_rx) = dev_ids; + Ok(Self { + device_ref, + cid: CID_BROADCAST, + report_rx, + dev_info: None, + }) + } + + pub fn is_u2f(&self) -> bool { + true + } + + 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 PartialEq for Device { + fn eq(&self, other_device: &Device) -> bool { + self.device_ref == other_device.device_ref + } +} + +impl Read for Device { + fn read(&mut self, mut bytes: &mut [u8]) -> io::Result<usize> { + let timeout = Duration::from_secs(READ_TIMEOUT); + let data = match self.report_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) + } +} + +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); + } +} diff --git a/third_party/rust/authenticator/src/macos/iokit.rs b/third_party/rust/authenticator/src/macos/iokit.rs new file mode 100644 index 0000000000..656cdb045d --- /dev/null +++ b/third_party/rust/authenticator/src/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/macos/mod.rs b/third_party/rust/authenticator/src/macos/mod.rs new file mode 100644 index 0000000000..44e85094d0 --- /dev/null +++ b/third_party/rust/authenticator/src/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/macos/monitor.rs b/third_party/rust/authenticator/src/macos/monitor.rs new file mode 100644 index 0000000000..189366f9d1 --- /dev/null +++ b/third_party/rust/authenticator/src/macos/monitor.rs @@ -0,0 +1,175 @@ +/* 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::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>>), &dyn Fn() -> bool) + Sync, +{ + manager: IOHIDManagerRef, + // Keep alive until the monitor goes away. + _matcher: IOHIDDeviceMatcher, + map: HashMap<IOHIDDeviceRef, DeviceData>, + new_device_cb: F, +} + +impl<F> Monitor<F> +where + F: Fn((IOHIDDeviceRef, Receiver<Vec<u8>>), &dyn Fn() -> bool) + Sync + 'static, +{ + pub fn new(new_device_cb: F) -> 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(), + } + } + + 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) { + // 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 { ref 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 (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), 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>>), &dyn Fn() -> bool) + Sync, +{ + fn drop(&mut self) { + unsafe { CFRelease(self.manager as *mut c_void) }; + } +} diff --git a/third_party/rust/authenticator/src/macos/transaction.rs b/third_party/rust/authenticator/src/macos/transaction.rs new file mode 100644 index 0000000000..697730a41a --- /dev/null +++ b/third_party/rust/authenticator/src/macos/transaction.rs @@ -0,0 +1,92 @@ +/* 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::platform::iokit::{CFRunLoopEntryObserver, IOHIDDeviceRef, SendableRunLoop}; +use crate::platform::monitor::Monitor; +use crate::statecallback::StateCallback; +use core_foundation::runloop::*; +use std::os::raw::c_void; +use std::sync::mpsc::{channel, Receiver, 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<()>>, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn((IOHIDDeviceRef, Receiver<Vec<u8>>), &dyn Fn() -> bool) + Sync + Send + 'static, + T: 'static, + { + let (tx, rx) = channel(); + let timeout = (timeout as f64) / 1000.0; + + 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); + 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), + }) + } + + 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()) }; + + // This must never be None. Ignore return value. + let _ = self.thread.take().unwrap().join(); + } +} diff --git a/third_party/rust/authenticator/src/manager.rs b/third_party/rust/authenticator/src/manager.rs new file mode 100644 index 0000000000..06f972dba4 --- /dev/null +++ b/third_party/rust/authenticator/src/manager.rs @@ -0,0 +1,197 @@ +/* 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::sync::mpsc::{channel, RecvTimeoutError, Sender}; +use std::time::Duration; + +use crate::authenticatorservice::AuthenticatorTransport; +use crate::consts::PARAMETER_SIZE; +use crate::errors::*; +use crate::statecallback::StateCallback; +use crate::statemachine::StateMachine; +use runloop::RunLoop; + +enum QueueAction { + Register { + flags: crate::RegisterFlags, + timeout: u64, + challenge: Vec<u8>, + application: crate::AppId, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + }, + Sign { + flags: crate::SignFlags, + timeout: u64, + challenge: Vec<u8>, + app_ids: Vec<crate::AppId>, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + }, + Cancel, +} + +pub struct U2FManager { + queue: RunLoop, + tx: Sender<QueueAction>, +} + +impl U2FManager { + pub fn new() -> io::Result<Self> { + let (tx, rx) = channel(); + + // Start a new work queue thread. + let queue = RunLoop::new(move |alive| { + let mut sm = StateMachine::new(); + + while alive() { + match rx.recv_timeout(Duration::from_millis(50)) { + Ok(QueueAction::Register { + flags, + timeout, + challenge, + application, + key_handles, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.register( + flags, + timeout, + challenge, + application, + key_handles, + status, + callback, + ); + } + Ok(QueueAction::Sign { + flags, + timeout, + challenge, + app_ids, + key_handles, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.sign( + flags, + timeout, + challenge, + app_ids, + key_handles, + status, + callback, + ); + } + Ok(QueueAction::Cancel) => { + // Cancelling must block so that we don't start a new + // polling thread before the old one has shut down. + sm.cancel(); + } + Err(RecvTimeoutError::Disconnected) => { + break; + } + _ => { /* continue */ } + } + } + + // Cancel any ongoing activity. + sm.cancel(); + })?; + + Ok(Self { queue, tx }) + } +} + +impl AuthenticatorTransport for U2FManager { + fn register( + &mut self, + flags: crate::RegisterFlags, + timeout: u64, + challenge: Vec<u8>, + application: crate::AppId, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for key_handle in &key_handles { + if key_handle.credential.len() > 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let action = QueueAction::Register { + flags, + timeout, + challenge, + application, + key_handles, + status, + callback, + }; + Ok(self.tx.send(action)?) + } + + fn sign( + &mut self, + flags: crate::SignFlags, + timeout: u64, + challenge: Vec<u8>, + app_ids: Vec<crate::AppId>, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + if challenge.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + if app_ids.is_empty() { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for app_id in &app_ids { + if app_id.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + for key_handle in &key_handles { + if key_handle.credential.len() > 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let action = QueueAction::Sign { + flags, + timeout, + challenge, + app_ids, + key_handles, + status, + callback, + }; + Ok(self.tx.send(action)?) + } + + fn cancel(&mut self) -> crate::Result<()> { + Ok(self.tx.send(QueueAction::Cancel)?) + } +} + +impl Drop for U2FManager { + fn drop(&mut self) { + self.queue.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/netbsd/device.rs b/third_party/rust/authenticator/src/netbsd/device.rs new file mode 100644 index 0000000000..92e7c22ea1 --- /dev/null +++ b/third_party/rust/authenticator/src/netbsd/device.rs @@ -0,0 +1,159 @@ +/* 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::io::Read; +use std::io::Write; +use std::mem; + +use crate::consts::CID_BROADCAST; +use crate::consts::MAX_HID_RPT_SIZE; +use crate::platform::fd::Fd; +use crate::platform::uhid; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::io_err; + +#[derive(Debug)] +pub struct Device { + fd: Fd, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, +} + +impl Device { + pub fn new(fd: Fd) -> io::Result<Self> { + Ok(Self { + fd, + cid: CID_BROADCAST, + dev_info: None, + }) + } + + pub 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 let Err(_) = self.ping() { + return false; + } + true + } + + 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 + + self.write(&buf[..])?; + + // 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 + self.read(&mut buf[..])?; + + return Ok(()); + } + + Err(io_err("no response from device")) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.fd == other.fd + } +} + +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<'a>(&'a self) -> &'a [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); + } +} diff --git a/third_party/rust/authenticator/src/netbsd/fd.rs b/third_party/rust/authenticator/src/netbsd/fd.rs new file mode 100644 index 0000000000..c011b7fcc8 --- /dev/null +++ b/third_party/rust/authenticator/src/netbsd/fd.rs @@ -0,0 +1,47 @@ +/* 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::io; +use std::mem; +use std::os::raw::c_int; +use std::os::unix::io::RawFd; + +#[derive(Debug)] +pub struct Fd { + pub fileno: RawFd, +} + +impl Fd { + pub fn open(path: &str, 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) + } +} diff --git a/third_party/rust/authenticator/src/netbsd/mod.rs b/third_party/rust/authenticator/src/netbsd/mod.rs new file mode 100644 index 0000000000..a0eabb6e06 --- /dev/null +++ b/third_party/rust/authenticator/src/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/netbsd/monitor.rs b/third_party/rust/authenticator/src/netbsd/monitor.rs new file mode 100644 index 0000000000..c78cff6ee1 --- /dev/null +++ b/third_party/rust/authenticator/src/netbsd/monitor.rs @@ -0,0 +1,87 @@ +/* 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::collections::HashMap; +use std::ffi::OsString; +use std::io; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use runloop::RunLoop; + +use crate::platform::fd::Fd; + +// 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; + +pub struct Monitor<F> +where + F: Fn(Fd, &dyn Fn() -> bool) + Send + Sync + 'static, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, +} + +impl<F> Monitor<F> +where + F: Fn(Fd, &dyn Fn() -> bool) + Send + Sync + 'static, +{ + pub fn new(new_device_cb: F) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> { + while alive() { + for n in 0..100 { + let uhidpath = format!("/dev/uhid{}", n); + match Fd::open(&uhidpath, libc::O_RDWR | libc::O_CLOEXEC) { + Ok(uhid) => { + self.add_device(uhid, OsString::from(&uhidpath)); + } + Err(ref err) => match err.raw_os_error() { + Some(libc::EBUSY) => continue, + Some(libc::ENOENT) => break, + _ => self.remove_device(OsString::from(&uhidpath)), + }, + } + } + thread::sleep(Duration::from_millis(POLL_TIMEOUT)); + } + self.remove_all_devices(); + Ok(()) + } + + fn add_device(&mut self, fd: Fd, path: OsString) { + let f = self.new_device_cb.clone(); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(fd, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(path.clone(), runloop); + } + } + + fn remove_device(&mut self, path: OsString) { + 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/netbsd/transaction.rs b/third_party/rust/authenticator/src/netbsd/transaction.rs new file mode 100644 index 0000000000..21ac212569 --- /dev/null +++ b/third_party/rust/authenticator/src/netbsd/transaction.rs @@ -0,0 +1,53 @@ +/* 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::platform::fd::Fd; +use crate::platform::monitor::Monitor; +use crate::statecallback::StateCallback; +use runloop::RunLoop; + +pub struct Transaction { + // Handle to the thread loop. + thread: Option<RunLoop>, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn(Fd, &dyn Fn() -> bool) + Sync + Send + 'static, + T: 'static, + { + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb); + + // 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: Some(thread), + }) + } + + pub fn cancel(&mut self) { + // This must never be None. + self.thread.take().unwrap().cancel(); + } +} diff --git a/third_party/rust/authenticator/src/netbsd/uhid.rs b/third_party/rust/authenticator/src/netbsd/uhid.rs new file mode 100644 index 0000000000..f8d711553d --- /dev/null +++ b/third_party/rust/authenticator/src/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::hidproto::has_fido_usage; +use crate::hidproto::ReportDescriptor; +use crate::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/openbsd/device.rs b/third_party/rust/authenticator/src/openbsd/device.rs new file mode 100644 index 0000000000..2238e034e2 --- /dev/null +++ b/third_party/rust/authenticator/src/openbsd/device.rs @@ -0,0 +1,148 @@ +/* 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::OsString; +use std::io; +use std::io::{Read, Result, Write}; +use std::mem; + +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::platform::monitor::FidoDev; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::{from_unix_result, io_err}; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: libc::c_int, + cid: [u8; 4], + out_len: usize, + dev_info: Option<U2FDeviceInfo>, +} + +impl Device { + pub fn new(fido: FidoDev) -> Result<Self> { + debug!("device found: {:?}", fido); + Ok(Self { + path: fido.os_path, + fd: fido.fd, + cid: CID_BROADCAST, + out_len: 64, + dev_info: None, + }) + } + + pub 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 ping(&mut self) -> Result<()> { + let capacity = 256; + + for _ in 0..10 { + let mut data = vec![0u8; capacity]; + + // Send 1 byte ping + 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 Read for Device { + fn read(&mut self, buf: &mut [u8]) -> 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]) -> 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) -> 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); + } +} diff --git a/third_party/rust/authenticator/src/openbsd/mod.rs b/third_party/rust/authenticator/src/openbsd/mod.rs new file mode 100644 index 0000000000..fa02132e67 --- /dev/null +++ b/third_party/rust/authenticator/src/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/openbsd/monitor.rs b/third_party/rust/authenticator/src/openbsd/monitor.rs new file mode 100644 index 0000000000..2f3930497f --- /dev/null +++ b/third_party/rust/authenticator/src/openbsd/monitor.rs @@ -0,0 +1,111 @@ +/* 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::collections::HashMap; +use std::ffi::{CString, OsString}; +use std::io; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::io::RawFd; +use std::path::PathBuf; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use crate::util::from_unix_result; +use runloop::RunLoop; + +const POLL_TIMEOUT: u64 = 500; + +#[derive(Debug)] +pub struct FidoDev { + pub fd: RawFd, + pub os_path: OsString, +} + +pub struct Monitor<F> +where + F: Fn(FidoDev, &dyn Fn() -> bool) + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, +} + +impl<F> Monitor<F> +where + F: Fn(FidoDev, &dyn Fn() -> bool) + Send + Sync + 'static, +{ + pub fn new(new_device_cb: F) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> { + // 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. + self.add_device(FidoDev { 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: FidoDev) { + if !self.runloops.contains_key(&fido.os_path) { + let f = self.new_device_cb.clone(); + let key = fido.os_path.clone(); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(fido, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + } + + fn remove_device(&mut self, path: OsString) { + 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/openbsd/transaction.rs b/third_party/rust/authenticator/src/openbsd/transaction.rs new file mode 100644 index 0000000000..4b85db2785 --- /dev/null +++ b/third_party/rust/authenticator/src/openbsd/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::platform::monitor::{FidoDev, Monitor}; +use crate::statecallback::StateCallback; +use runloop::RunLoop; + +pub struct Transaction { + // Handle to the thread loop. + thread: Option<RunLoop>, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn(FidoDev, &dyn Fn() -> bool) + Sync + Send + 'static, + T: 'static, + { + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb); + + // 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: Some(thread), + }) + } + + pub fn cancel(&mut self) { + // This must never be None. + self.thread.take().unwrap().cancel(); + } +} diff --git a/third_party/rust/authenticator/src/statecallback.rs b/third_party/rust/authenticator/src/statecallback.rs new file mode 100644 index 0000000000..ce1caf3e7c --- /dev/null +++ b/third_party/rust/authenticator/src/statecallback.rs @@ -0,0 +1,166 @@ +/* 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::sync::{Arc, Condvar, Mutex}; + +pub struct StateCallback<T> { + callback: Arc<Mutex<Option<Box<dyn Fn(T) + Send>>>>, + observer: Arc<Mutex<Option<Box<dyn Fn() + Send>>>>, + condition: Arc<(Mutex<bool>, Condvar)>, +} + +impl<T> StateCallback<T> { + // This is used for the Condvar, which requires this kind of construction + #[allow(clippy::mutex_atomic)] + pub fn new(cb: Box<dyn Fn(T) + Send>) -> Self { + Self { + callback: Arc::new(Mutex::new(Some(cb))), + observer: Arc::new(Mutex::new(None)), + condition: Arc::new((Mutex::new(true), Condvar::new())), + } + } + + pub fn add_uncloneable_observer(&mut self, obs: Box<dyn Fn() + Send>) { + let mut opt = self.observer.lock().unwrap(); + if opt.is_some() { + error!("Replacing an already-set observer.") + } + opt.replace(obs); + } + + pub fn call(&self, rv: T) { + if let Some(cb) = self.callback.lock().unwrap().take() { + cb(rv); + + if let Some(obs) = self.observer.lock().unwrap().take() { + obs(); + } + } + + let (lock, cvar) = &*self.condition; + let mut pending = lock.lock().unwrap(); + *pending = false; + cvar.notify_all(); + } + + pub fn wait(&self) { + let (lock, cvar) = &*self.condition; + let _useless_guard = cvar + .wait_while(lock.lock().unwrap(), |pending| *pending) + .unwrap(); + } +} + +impl<T> Clone for StateCallback<T> { + fn clone(&self) -> Self { + Self { + callback: self.callback.clone(), + observer: Arc::new(Mutex::new(None)), + condition: self.condition.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::StateCallback; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Barrier}; + use std::thread; + + #[test] + fn test_statecallback_is_single_use() { + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + let sc = StateCallback::new(Box::new(move |_| { + counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + assert_eq!(counter.load(Ordering::SeqCst), 0); + for _ in 0..10 { + sc.call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + for _ in 0..10 { + sc.clone().call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn test_statecallback_observer_is_single_use() { + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + let mut sc = StateCallback::<()>::new(Box::new(move |_| {})); + + sc.add_uncloneable_observer(Box::new(move || { + counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + assert_eq!(counter.load(Ordering::SeqCst), 0); + for _ in 0..10 { + sc.call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + for _ in 0..10 { + sc.clone().call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn test_statecallback_observer_only_runs_for_completing_callback() { + let cb_counter = Arc::new(AtomicUsize::new(0)); + let cb_counter_clone = cb_counter.clone(); + let sc = StateCallback::new(Box::new(move |_| { + cb_counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + let obs_counter = Arc::new(AtomicUsize::new(0)); + + for _ in 0..10 { + let obs_counter_clone = obs_counter.clone(); + let mut c = sc.clone(); + c.add_uncloneable_observer(Box::new(move || { + obs_counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + c.call(()); + + assert_eq!(cb_counter.load(Ordering::SeqCst), 1); + assert_eq!(obs_counter.load(Ordering::SeqCst), 1); + } + } + + #[test] + #[allow(clippy::redundant_clone)] + fn test_statecallback_observer_unclonable() { + let mut sc = StateCallback::<()>::new(Box::new(move |_| {})); + sc.add_uncloneable_observer(Box::new(move || {})); + + assert!(sc.observer.lock().unwrap().is_some()); + // This is deliberate, to force an extra clone + assert!(sc.clone().observer.lock().unwrap().is_none()); + } + + #[test] + fn test_statecallback_wait() { + let sc = StateCallback::<()>::new(Box::new(move |_| {})); + let barrier = Arc::new(Barrier::new(2)); + + { + let c = sc.clone(); + let b = barrier.clone(); + thread::spawn(move || { + b.wait(); + c.call(()); + }); + } + + barrier.wait(); + sc.wait(); + } +} diff --git a/third_party/rust/authenticator/src/statemachine.rs b/third_party/rust/authenticator/src/statemachine.rs new file mode 100644 index 0000000000..32552f8d0a --- /dev/null +++ b/third_party/rust/authenticator/src/statemachine.rs @@ -0,0 +1,283 @@ +/* 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::PARAMETER_SIZE; +use crate::errors; +use crate::platform::device::Device; +use crate::platform::transaction::Transaction; +use crate::statecallback::StateCallback; +use crate::u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign}; +use crate::u2ftypes::U2FDevice; + +use std::sync::mpsc::Sender; +use std::sync::Mutex; +use std::thread; +use std::time::Duration; + +fn is_valid_transport(transports: crate::AuthenticatorTransports) -> bool { + transports.is_empty() || transports.contains(crate::AuthenticatorTransports::USB) +} + +fn find_valid_key_handles<'a, F>( + app_ids: &'a [crate::AppId], + key_handles: &'a [crate::KeyHandle], + mut is_valid: F, +) -> (&'a crate::AppId, Vec<&'a crate::KeyHandle>) +where + F: FnMut(&Vec<u8>, &crate::KeyHandle) -> bool, +{ + // Try all given app_ids in order. + for app_id in app_ids { + // Find all valid key handles for the current app_id. + let valid_handles = key_handles + .iter() + .filter(|key_handle| is_valid(app_id, key_handle)) + .collect::<Vec<_>>(); + + // If there's at least one, stop. + if !valid_handles.is_empty() { + return (app_id, valid_handles); + } + } + + (&app_ids[0], vec![]) +} + +fn send_status(status_mutex: &Mutex<Sender<crate::StatusUpdate>>, msg: crate::StatusUpdate) { + match status_mutex.lock() { + Ok(s) => match s.send(msg) { + Ok(_) => {} + Err(e) => error!("Couldn't send status: {:?}", e), + }, + Err(e) => { + error!("Couldn't obtain status mutex: {:?}", e); + } + }; +} + +#[derive(Default)] +pub struct StateMachine { + transaction: Option<Transaction>, +} + +impl StateMachine { + pub fn new() -> Self { + Default::default() + } + + pub fn register( + &mut self, + flags: crate::RegisterFlags, + timeout: u64, + challenge: Vec<u8>, + application: crate::AppId, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + + let cbc = callback.clone(); + let status_mutex = Mutex::new(status); + + let transaction = Transaction::new(timeout, cbc.clone(), move |info, alive| { + // Create a new device. + let dev = &mut match Device::new(info) { + Ok(dev) => dev, + _ => return, + }; + + // Try initializing it. + if !dev.is_u2f() || !u2f_init_device(dev) { + return; + } + + // We currently support none of the authenticator selection + // criteria because we can't ask tokens whether they do support + // those features. If flags are set, ignore all tokens for now. + // + // Technically, this is a ConstraintError because we shouldn't talk + // to this authenticator in the first place. But the result is the + // same anyway. + if !flags.is_empty() { + return; + } + + send_status( + &status_mutex, + crate::StatusUpdate::DeviceAvailable { + dev_info: dev.get_device_info(), + }, + ); + + // Iterate the exclude list and see if there are any matches. + // If so, we'll keep polling the device anyway to test for user + // consent, to be consistent with CTAP2 device behavior. + let excluded = key_handles.iter().any(|key_handle| { + is_valid_transport(key_handle.transports) + && u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential) + .unwrap_or(false) /* no match on failure */ + }); + + while alive() { + if excluded { + let blank = vec![0u8; PARAMETER_SIZE]; + if u2f_register(dev, &blank, &blank).is_ok() { + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::InvalidState, + ))); + break; + } + } else if let Ok(bytes) = u2f_register(dev, &challenge, &application) { + let dev_info = dev.get_device_info(); + send_status( + &status_mutex, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + callback.call(Ok((bytes, dev_info))); + break; + } + + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } + + send_status( + &status_mutex, + crate::StatusUpdate::DeviceUnavailable { + dev_info: dev.get_device_info(), + }, + ); + }); + + self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + } + + pub fn sign( + &mut self, + flags: crate::SignFlags, + timeout: u64, + challenge: Vec<u8>, + app_ids: Vec<crate::AppId>, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + + let cbc = callback.clone(); + + let status_mutex = Mutex::new(status); + + let transaction = Transaction::new(timeout, cbc.clone(), move |info, alive| { + // Create a new device. + let dev = &mut match Device::new(info) { + Ok(dev) => dev, + _ => return, + }; + + // Try initializing it. + if !dev.is_u2f() || !u2f_init_device(dev) { + return; + } + + // We currently don't support user verification because we can't + // ask tokens whether they do support that. If the flag is set, + // ignore all tokens for now. + // + // Technically, this is a ConstraintError because we shouldn't talk + // to this authenticator in the first place. But the result is the + // same anyway. + if !flags.is_empty() { + return; + } + + // For each appId, try all key handles. If there's at least one + // valid key handle for an appId, we'll use that appId below. + let (app_id, valid_handles) = + find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| { + u2f_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential) + .unwrap_or(false) /* no match on failure */ + }); + + // Aggregate distinct transports from all given credentials. + let transports = key_handles + .iter() + .fold(crate::AuthenticatorTransports::empty(), |t, k| { + t | k.transports + }); + + // We currently only support USB. If the RP specifies transports + // and doesn't include USB it's probably lying. + if !is_valid_transport(transports) { + return; + } + + send_status( + &status_mutex, + crate::StatusUpdate::DeviceAvailable { + dev_info: dev.get_device_info(), + }, + ); + + 'outer: while alive() { + // If the device matches none of the given key handles + // then just make it blink with bogus data. + if valid_handles.is_empty() { + let blank = vec![0u8; PARAMETER_SIZE]; + if u2f_register(dev, &blank, &blank).is_ok() { + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::InvalidState, + ))); + break; + } + } else { + // Otherwise, try to sign. + for key_handle in &valid_handles { + if let Ok(bytes) = u2f_sign(dev, &challenge, app_id, &key_handle.credential) + { + let dev_info = dev.get_device_info(); + send_status( + &status_mutex, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + callback.call(Ok(( + app_id.clone(), + key_handle.credential.clone(), + bytes, + dev_info, + ))); + break 'outer; + } + } + } + + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } + + send_status( + &status_mutex, + crate::StatusUpdate::DeviceUnavailable { + dev_info: dev.get_device_info(), + }, + ); + }); + + self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + } + + // This blocks. + pub fn cancel(&mut self) { + if let Some(mut transaction) = self.transaction.take() { + transaction.cancel(); + } + } +} diff --git a/third_party/rust/authenticator/src/stub/device.rs b/third_party/rust/authenticator/src/stub/device.rs new file mode 100644 index 0000000000..283da8ed66 --- /dev/null +++ b/third_party/rust/authenticator/src/stub/device.rs @@ -0,0 +1,65 @@ +/* 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::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use std::io; +use std::io::{Read, Write}; + +pub struct Device {} + +impl Device { + pub fn new(path: String) -> io::Result<Self> { + panic!("not implemented"); + } + + pub fn is_u2f(&self) -> bool { + panic!("not implemented"); + } +} + +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<'a>(&'a self) -> &'a [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") + } +} diff --git a/third_party/rust/authenticator/src/stub/mod.rs b/third_party/rust/authenticator/src/stub/mod.rs new file mode 100644 index 0000000000..0fab62d495 --- /dev/null +++ b/third_party/rust/authenticator/src/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/stub/transaction.rs b/third_party/rust/authenticator/src/stub/transaction.rs new file mode 100644 index 0000000000..bdf48ef56d --- /dev/null +++ b/third_party/rust/authenticator/src/stub/transaction.rs @@ -0,0 +1,31 @@ +/* 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; + +pub struct Transaction {} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn(String, &dyn Fn() -> bool), + { + 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/u2fhid-capi.h b/third_party/rust/authenticator/src/u2fhid-capi.h new file mode 100644 index 0000000000..bb81b28f5f --- /dev/null +++ b/third_party/rust/authenticator/src/u2fhid-capi.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=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/. */ + +#ifndef __U2FHID_CAPI +#define __U2FHID_CAPI +#include <stdlib.h> +#include "nsString.h" + +extern "C" { + +const uint8_t U2F_RESBUF_ID_REGISTRATION = 0; +const uint8_t U2F_RESBUF_ID_KEYHANDLE = 1; +const uint8_t U2F_RESBUF_ID_SIGNATURE = 2; +const uint8_t U2F_RESBUF_ID_APPID = 3; +const uint8_t U2F_RESBUF_ID_VENDOR_NAME = 4; +const uint8_t U2F_RESBUF_ID_DEVICE_NAME = 5; +const uint8_t U2F_RESBUF_ID_FIRMWARE_MAJOR = 6; +const uint8_t U2F_RESBUF_ID_FIRMWARE_MINOR = 7; +const uint8_t U2F_RESBUF_ID_FIRMWARE_BUILD = 8; + +const uint64_t U2F_FLAG_REQUIRE_RESIDENT_KEY = 1; +const uint64_t U2F_FLAG_REQUIRE_USER_VERIFICATION = 2; +const uint64_t U2F_FLAG_REQUIRE_PLATFORM_ATTACHMENT = 4; + +const uint8_t U2F_AUTHENTICATOR_TRANSPORT_USB = 1; +const uint8_t U2F_AUTHENTICATOR_TRANSPORT_NFC = 2; +const uint8_t U2F_AUTHENTICATOR_TRANSPORT_BLE = 4; +const uint8_t CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL = 8; + +const uint8_t U2F_ERROR_UKNOWN = 1; +const uint8_t U2F_ERROR_NOT_SUPPORTED = 2; +const uint8_t U2F_ERROR_INVALID_STATE = 3; +const uint8_t U2F_ERROR_CONSTRAINT = 4; +const uint8_t U2F_ERROR_NOT_ALLOWED = 5; + +// NOTE: Preconditions +// * All rust_u2f_mgr* pointers must refer to pointers which are returned +// by rust_u2f_mgr_new, and must be freed with rust_u2f_mgr_free. +// * All rust_u2f_khs* pointers must refer to pointers which are returned +// by rust_u2f_khs_new, and must be freed with rust_u2f_khs_free. +// * All rust_u2f_res* pointers must refer to pointers passed to the +// register() and sign() callbacks. They can be null on failure. + +// The `rust_u2f_mgr` opaque type is equivalent to the rust type `U2FManager` +struct rust_u2f_manager; + +// The `rust_u2f_app_ids` opaque type is equivalent to the rust type `U2FAppIds` +struct rust_u2f_app_ids; + +// The `rust_u2f_key_handles` opaque type is equivalent to the rust type +// `U2FKeyHandles` +struct rust_u2f_key_handles; + +// The `rust_u2f_res` opaque type is equivalent to the rust type `U2FResult` +struct rust_u2f_result; + +// The callback passed to register() and sign(). +typedef void (*rust_u2f_callback)(uint64_t, rust_u2f_result*); + +/// U2FManager functions. + +rust_u2f_manager* rust_u2f_mgr_new(); +/* unsafe */ void rust_u2f_mgr_free(rust_u2f_manager* mgr); + +uint64_t rust_u2f_mgr_register(rust_u2f_manager* mgr, uint64_t flags, + uint64_t timeout, rust_u2f_callback, + const uint8_t* challenge_ptr, + size_t challenge_len, + const uint8_t* application_ptr, + size_t application_len, + const rust_u2f_key_handles* khs); + +uint64_t rust_u2f_mgr_sign(rust_u2f_manager* mgr, uint64_t flags, + uint64_t timeout, rust_u2f_callback, + const uint8_t* challenge_ptr, size_t challenge_len, + const rust_u2f_app_ids* app_ids, + const rust_u2f_key_handles* khs); + +void rust_u2f_mgr_cancel(rust_u2f_manager* mgr); + +/// U2FAppIds functions. + +rust_u2f_app_ids* rust_u2f_app_ids_new(); +void rust_u2f_app_ids_add(rust_u2f_app_ids* ids, const uint8_t* id, + size_t id_len); +/* unsafe */ void rust_u2f_app_ids_free(rust_u2f_app_ids* ids); + +/// U2FKeyHandles functions. + +rust_u2f_key_handles* rust_u2f_khs_new(); +void rust_u2f_khs_add(rust_u2f_key_handles* khs, const uint8_t* key_handle, + size_t key_handle_len, uint8_t transports); +/* unsafe */ void rust_u2f_khs_free(rust_u2f_key_handles* khs); + +/// U2FResult functions. + +// Returns 0 for success, or the U2F_ERROR error code >= 1. +uint8_t rust_u2f_result_error(const rust_u2f_result* res); + +// Call this before `[..]_copy()` to allocate enough space. +bool rust_u2f_resbuf_length(const rust_u2f_result* res, uint8_t bid, + size_t* len); +bool rust_u2f_resbuf_copy(const rust_u2f_result* res, uint8_t bid, + uint8_t* dst); +/* unsafe */ void rust_u2f_res_free(rust_u2f_result* res); +} + +#endif // __U2FHID_CAPI diff --git a/third_party/rust/authenticator/src/u2fprotocol.rs b/third_party/rust/authenticator/src/u2fprotocol.rs new file mode 100644 index 0000000000..ca9f704387 --- /dev/null +++ b/third_party/rust/authenticator/src/u2fprotocol.rs @@ -0,0 +1,457 @@ +/* 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::needless_lifetimes))] + +extern crate std; + +use rand::{thread_rng, RngCore}; +use std::ffi::CString; +use std::io; +use std::io::{Read, Write}; + +use crate::consts::*; +use crate::u2ftypes::*; +use crate::util::io_err; + +//////////////////////////////////////////////////////////////////////// +// Device Commands +//////////////////////////////////////////////////////////////////////// + +pub fn u2f_init_device<T>(dev: &mut T) -> bool +where + T: U2FDevice + Read + Write, +{ + let mut nonce = [0u8; 8]; + thread_rng().fill_bytes(&mut nonce); + + // Initialize the device and check its version. + init_device(dev, &nonce).is_ok() && is_v2_device(dev).unwrap_or(false) +} + +pub fn u2f_register<T>(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result<Vec<u8>> +where + T: U2FDevice + Read + Write, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); + register_data.extend(challenge); + register_data.extend(application); + + let flags = U2F_REQUEST_USER_PRESENCE; + let (resp, status) = send_apdu(dev, U2F_REGISTER, flags, ®ister_data)?; + status_word_to_result(status, resp) +} + +pub fn u2f_sign<T>( + dev: &mut T, + challenge: &[u8], + application: &[u8], + key_handle: &[u8], +) -> io::Result<Vec<u8>> +where + T: U2FDevice + Read + Write, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + if key_handle.len() > 256 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Key handle too large", + )); + } + + let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); + sign_data.extend(challenge); + sign_data.extend(application); + sign_data.push(key_handle.len() as u8); + sign_data.extend(key_handle); + + let flags = U2F_REQUEST_USER_PRESENCE; + let (resp, status) = send_apdu(dev, U2F_AUTHENTICATE, flags, &sign_data)?; + status_word_to_result(status, resp) +} + +pub fn u2f_is_keyhandle_valid<T>( + dev: &mut T, + challenge: &[u8], + application: &[u8], + key_handle: &[u8], +) -> io::Result<bool> +where + T: U2FDevice + Read + Write, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + if key_handle.len() > 256 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Key handle too large", + )); + } + + let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); + sign_data.extend(challenge); + sign_data.extend(application); + sign_data.push(key_handle.len() as u8); + sign_data.extend(key_handle); + + let flags = U2F_CHECK_IS_REGISTERED; + let (_, status) = send_apdu(dev, U2F_AUTHENTICATE, flags, &sign_data)?; + Ok(status == SW_CONDITIONS_NOT_SATISFIED) +} + +//////////////////////////////////////////////////////////////////////// +// Internal Device Commands +//////////////////////////////////////////////////////////////////////// + +fn init_device<T>(dev: &mut T, nonce: &[u8]) -> io::Result<()> +where + T: U2FDevice + Read + Write, +{ + assert_eq!(nonce.len(), INIT_NONCE_SIZE); + let raw = sendrecv(dev, U2FHID_INIT, nonce)?; + let rsp = U2FHIDInitResp::read(&raw, nonce)?; + dev.set_cid(rsp.cid); + + let vendor = dev + .get_property("Manufacturer") + .unwrap_or_else(|_| String::from("Unknown Vendor")); + let product = dev + .get_property("Product") + .unwrap_or_else(|_| String::from("Unknown Device")); + + dev.set_device_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, + }); + + Ok(()) +} + +fn is_v2_device<T>(dev: &mut T) -> io::Result<bool> +where + T: U2FDevice + Read + Write, +{ + let (data, status) = send_apdu(dev, U2F_VERSION, 0x00, &[])?; + let actual = CString::new(data)?; + let expected = CString::new("U2F_V2")?; + status_word_to_result(status, actual == expected) +} + +//////////////////////////////////////////////////////////////////////// +// Error Handling +//////////////////////////////////////////////////////////////////////// + +fn status_word_to_result<T>(status: [u8; 2], val: T) -> io::Result<T> { + use self::io::ErrorKind::{InvalidData, InvalidInput}; + + match status { + SW_NO_ERROR => Ok(val), + SW_WRONG_DATA => Err(io::Error::new(InvalidData, "wrong data")), + SW_WRONG_LENGTH => Err(io::Error::new(InvalidInput, "wrong length")), + SW_CONDITIONS_NOT_SATISFIED => Err(io_err("conditions not satisfied")), + _ => Err(io_err(&format!("failed with status {:?}", status))), + } +} + +//////////////////////////////////////////////////////////////////////// +// Device Communication Functions +//////////////////////////////////////////////////////////////////////// + +pub fn sendrecv<T>(dev: &mut T, cmd: u8, send: &[u8]) -> io::Result<Vec<u8>> +where + T: U2FDevice + Read + Write, +{ + // Send initialization packet. + let mut count = U2FHIDInit::write(dev, cmd, send)?; + + // Send continuation packets. + let mut sequence = 0u8; + while count < send.len() { + count += U2FHIDCont::write(dev, sequence, &send[count..])?; + sequence += 1; + } + + // 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 mut data = U2FHIDInit::read(dev)?; + + let mut sequence = 0u8; + while data.len() < data.capacity() { + let max = data.capacity() - data.len(); + data.extend_from_slice(&U2FHIDCont::read(dev, sequence, max)?); + sequence += 1; + } + + Ok(data) +} + +fn send_apdu<T>(dev: &mut T, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec<u8>, [u8; 2])> +where + T: U2FDevice + Read + Write, +{ + let apdu = U2FAPDUHeader::serialize(cmd, p1, send)?; + let mut data = sendrecv(dev, U2FHID_MSG, &apdu)?; + + if data.len() < 2 { + return Err(io_err("unexpected response")); + } + + let split_at = data.len() - 2; + let status = data.split_off(split_at); + Ok((data, [status[0], status[1]])) +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use rand::{thread_rng, RngCore}; + + use super::{init_device, send_apdu, sendrecv, U2FDevice}; + use crate::consts::{CID_BROADCAST, SW_NO_ERROR, U2FHID_INIT, U2FHID_MSG, U2FHID_PING}; + + mod platform { + use std::io; + use std::io::{Read, Write}; + + use crate::consts::CID_BROADCAST; + use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; + + const IN_HID_RPT_SIZE: usize = 64; + const OUT_HID_RPT_SIZE: usize = 64; + + pub struct TestDevice { + cid: [u8; 4], + reads: Vec<[u8; IN_HID_RPT_SIZE]>, + writes: Vec<[u8; OUT_HID_RPT_SIZE + 1]>, + dev_info: Option<U2FDeviceInfo>, + } + + impl TestDevice { + pub fn new() -> TestDevice { + TestDevice { + cid: CID_BROADCAST, + reads: vec![], + writes: vec![], + dev_info: None, + } + } + + 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); + } + } + + impl Write for TestDevice { + 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!"); + 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 TestDevice { + 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 TestDevice { + fn drop(&mut self) { + assert!(self.reads.is_empty()); + assert!(self.writes.is_empty()); + } + } + + impl U2FDevice for TestDevice { + fn get_cid<'a>(&'a self) -> &'a [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!("{} not implemented", prop_name)) + } + 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); + } + } + } + + #[test] + fn test_init_device() { + let mut device = platform::TestDevice::new(); + let nonce = vec![0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; + + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + // init packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![U2FHID_INIT, 0x00, 0x08]); // cmd + bcnt + msg.extend_from_slice(&nonce); + device.add_write(&msg, 0); + + // init_resp packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![U2FHID_INIT, 0x00, 0x11]); // cmd + bcnt + msg.extend_from_slice(&nonce); + msg.extend_from_slice(&cid); // new channel id + msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags + device.add_read(&msg, 0); + + init_device(&mut device, &nonce).unwrap(); + assert_eq!(device.get_cid(), &cid); + + let dev_info = device.get_device_info(); + assert_eq!(dev_info.version_interface, 0x02); + assert_eq!(dev_info.version_major, 0x04); + assert_eq!(dev_info.version_minor, 0x01); + assert_eq!(dev_info.version_build, 0x08); + assert_eq!(dev_info.cap_flags, 0x01); + } + + #[test] + fn test_sendrecv_multiple() { + let mut device = platform::TestDevice::new(); + let cid = [0x01, 0x02, 0x03, 0x04]; + device.set_cid(cid); + + // init packet + let mut msg = cid.to_vec(); + msg.extend(vec![U2FHID_PING, 0x00, 0xe4]); // cmd + length = 228 + // write msg, append [1u8; 57], 171 bytes remain + device.add_write(&msg, 1); + device.add_read(&msg, 1); + + // cont packet + let mut msg = cid.to_vec(); + msg.push(0x00); // seq = 0 + // write msg, append [1u8; 59], 112 bytes remaining + device.add_write(&msg, 1); + device.add_read(&msg, 1); + + // cont packet + let mut msg = cid.to_vec(); + msg.push(0x01); // seq = 1 + // write msg, append [1u8; 59], 53 bytes remaining + device.add_write(&msg, 1); + device.add_read(&msg, 1); + + // cont packet + let mut msg = cid.to_vec(); + msg.push(0x02); // seq = 2 + msg.extend_from_slice(&[1u8; 53]); + // write msg, append remaining 53 bytes. + device.add_write(&msg, 0); + device.add_read(&msg, 0); + + let data = [1u8; 228]; + let d = sendrecv(&mut device, U2FHID_PING, &data).unwrap(); + assert_eq!(d.len(), 228); + assert_eq!(d, &data[..]); + } + + #[test] + fn test_sendapdu() { + let cid = [0x01, 0x02, 0x03, 0x04]; + let data = [0x01, 0x02, 0x03, 0x04, 0x05]; + let mut device = platform::TestDevice::new(); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + // sendrecv header + msg.extend(vec![U2FHID_MSG, 0x00, 0x0e]); // len = 14 + // apdu header + msg.extend(vec![0x00, U2FHID_PING, 0xaa, 0x00, 0x00, 0x00, 0x05]); + // apdu data + msg.extend_from_slice(&data); + device.add_write(&msg, 0); + + // Send data back + let mut msg = cid.to_vec(); + msg.extend(vec![U2FHID_MSG, 0x00, 0x07]); + msg.extend_from_slice(&data); + msg.extend_from_slice(&SW_NO_ERROR); + device.add_read(&msg, 0); + + let (result, status) = send_apdu(&mut device, U2FHID_PING, 0xaa, &data).unwrap(); + assert_eq!(result, &data); + assert_eq!(status, SW_NO_ERROR); + } + + #[test] + fn test_get_property() { + let device = platform::TestDevice::new(); + + assert_eq!(device.get_property("a").unwrap(), "a not implemented"); + } +} diff --git a/third_party/rust/authenticator/src/u2ftypes.rs b/third_party/rust/authenticator/src/u2ftypes.rs new file mode 100644 index 0000000000..8360e8adbd --- /dev/null +++ b/third_party/rust/authenticator/src/u2ftypes.rs @@ -0,0 +1,256 @@ +/* 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::{cmp, fmt, io, str}; + +use crate::consts::*; +use crate::util::io_err; + +pub fn to_hex(data: &[u8], joiner: &str) -> String { + let parts: Vec<String> = data.iter().map(|byte| format!("{:02x}", byte)).collect(); + parts.join(joiner) +} + +pub fn trace_hex(data: &[u8]) { + if log_enabled!(log::Level::Trace) { + trace!("USB send: {}", to_hex(data, "")); + } +} + +// Trait for representing U2F HID Devices. Requires getters/setters for the +// channel ID, created during device initialization. +pub trait U2FDevice { + fn get_cid(&self) -> &[u8; 4]; + fn set_cid(&mut self, cid: [u8; 4]); + + fn in_rpt_size(&self) -> usize; + fn in_init_data_size(&self) -> usize { + self.in_rpt_size() - INIT_HEADER_SIZE + } + fn in_cont_data_size(&self) -> usize { + self.in_rpt_size() - CONT_HEADER_SIZE + } + + fn out_rpt_size(&self) -> usize; + fn out_init_data_size(&self) -> usize { + self.out_rpt_size() - INIT_HEADER_SIZE + } + fn out_cont_data_size(&self) -> usize { + self.out_rpt_size() - CONT_HEADER_SIZE + } + + fn get_property(&self, prop_name: &str) -> io::Result<String>; + fn get_device_info(&self) -> U2FDeviceInfo; + fn set_device_info(&mut self, dev_info: U2FDeviceInfo); +} + +// Init structure for U2F Communications. Tells the receiver what channel +// communication is happening on, what command is running, and how much data to +// expect to receive over all. +// +// Spec at https://fidoalliance.org/specs/fido-u2f-v1. +// 0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.html#message--and-packet-structure +pub struct U2FHIDInit {} + +impl U2FHIDInit { + pub fn read<T>(dev: &mut T) -> io::Result<Vec<u8>> + where + T: U2FDevice + io::Read, + { + let mut frame = vec![0u8; dev.in_rpt_size()]; + let mut count = dev.read(&mut frame)?; + + while dev.get_cid() != &frame[..4] { + count = dev.read(&mut frame)?; + } + + if count != dev.in_rpt_size() { + return Err(io_err("invalid init packet")); + } + + let cap = (frame[5] as usize) << 8 | (frame[6] as usize); + let mut data = Vec::with_capacity(cap); + + let len = cmp::min(cap, dev.in_init_data_size()); + data.extend_from_slice(&frame[7..7 + len]); + + Ok(data) + } + + pub fn write<T>(dev: &mut T, cmd: u8, data: &[u8]) -> io::Result<usize> + where + T: U2FDevice + io::Write, + { + if data.len() > 0xffff { + return Err(io_err("payload length > 2^16")); + } + + let mut frame = vec![0u8; dev.out_rpt_size() + 1]; + frame[1..5].copy_from_slice(dev.get_cid()); + frame[5] = cmd; + frame[6] = (data.len() >> 8) as u8; + frame[7] = data.len() as u8; + + let count = cmp::min(data.len(), dev.out_init_data_size()); + frame[8..8 + count].copy_from_slice(&data[..count]); + trace_hex(&frame); + + if dev.write(&frame)? != frame.len() { + return Err(io_err("device write failed")); + } + + Ok(count) + } +} + +// Continuation structure for U2F Communications. After an Init structure is +// sent, continuation structures are used to transmit all extra data that +// wouldn't fit in the initial packet. The sequence number increases with every +// packet, until all data is received. +// +// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol. +// html#message--and-packet-structure +pub struct U2FHIDCont {} + +impl U2FHIDCont { + pub fn read<T>(dev: &mut T, seq: u8, max: usize) -> io::Result<Vec<u8>> + where + T: U2FDevice + io::Read, + { + let mut frame = vec![0u8; dev.in_rpt_size()]; + let mut count = dev.read(&mut frame)?; + + while dev.get_cid() != &frame[..4] { + count = dev.read(&mut frame)?; + } + + if count != dev.in_rpt_size() { + return Err(io_err("invalid cont packet")); + } + + if seq != frame[4] { + return Err(io_err("invalid sequence number")); + } + + let max = cmp::min(max, dev.in_cont_data_size()); + Ok(frame[5..5 + max].to_vec()) + } + + pub fn write<T>(dev: &mut T, seq: u8, data: &[u8]) -> io::Result<usize> + where + T: U2FDevice + io::Write, + { + let mut frame = vec![0u8; dev.out_rpt_size() + 1]; + frame[1..5].copy_from_slice(dev.get_cid()); + frame[5] = seq; + + let count = cmp::min(data.len(), dev.out_cont_data_size()); + frame[6..6 + count].copy_from_slice(&data[..count]); + trace_hex(&frame); + + if dev.write(&frame)? != frame.len() { + return Err(io_err("device write failed")); + } + + Ok(count) + } +} + +// Reply sent after initialization command. Contains information about U2F USB +// Key versioning, as well as the communication channel to be used for all +// further requests. +// +// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol. +// html#u2fhid_init +pub struct U2FHIDInitResp { + pub cid: [u8; 4], + pub version_interface: u8, + pub version_major: u8, + pub version_minor: u8, + pub version_build: u8, + pub cap_flags: u8, +} + +impl U2FHIDInitResp { + pub fn read(data: &[u8], nonce: &[u8]) -> io::Result<U2FHIDInitResp> { + assert_eq!(nonce.len(), INIT_NONCE_SIZE); + + if data.len() != INIT_NONCE_SIZE + 9 { + return Err(io_err("invalid init response")); + } + + if nonce != &data[..INIT_NONCE_SIZE] { + return Err(io_err("invalid nonce")); + } + + let rsp = U2FHIDInitResp { + cid: [ + data[INIT_NONCE_SIZE], + data[INIT_NONCE_SIZE + 1], + data[INIT_NONCE_SIZE + 2], + data[INIT_NONCE_SIZE + 3], + ], + version_interface: data[INIT_NONCE_SIZE + 4], + version_major: data[INIT_NONCE_SIZE + 5], + version_minor: data[INIT_NONCE_SIZE + 6], + version_build: data[INIT_NONCE_SIZE + 7], + cap_flags: data[INIT_NONCE_SIZE + 8], + }; + + Ok(rsp) + } +} + +// https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit +// https://fidoalliance.org/specs/fido-u2f-v1. +// 0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#u2f-message-framing +pub struct U2FAPDUHeader {} + +impl U2FAPDUHeader { + pub fn serialize(ins: u8, p1: u8, data: &[u8]) -> io::Result<Vec<u8>> { + if data.len() > 0xffff { + return Err(io_err("payload length > 2^16")); + } + + // Size of header + data + 2 zero bytes for maximum return size. + let mut bytes = vec![0u8; U2FAPDUHEADER_SIZE + data.len() + 2]; + // cla is always 0 for our requirements + bytes[1] = ins; + bytes[2] = p1; + // p2 is always 0, at least, for our requirements. + // lc[0] should always be 0. + bytes[5] = (data.len() >> 8) as u8; + bytes[6] = data.len() as u8; + bytes[7..7 + data.len()].copy_from_slice(data); + + Ok(bytes) + } +} + +#[derive(Clone, Debug)] +pub struct U2FDeviceInfo { + pub vendor_name: Vec<u8>, + pub device_name: Vec<u8>, + pub version_interface: u8, + pub version_major: u8, + pub version_minor: u8, + pub version_build: u8, + pub cap_flags: u8, +} + +impl fmt::Display for U2FDeviceInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Vendor: {}, Device: {}, Interface: {}, Firmware: v{}.{}.{}, Capabilities: {}", + str::from_utf8(&self.vendor_name).unwrap(), + str::from_utf8(&self.device_name).unwrap(), + &self.version_interface, + &self.version_major, + &self.version_minor, + &self.version_build, + to_hex(&[self.cap_flags], ":"), + ) + } +} diff --git a/third_party/rust/authenticator/src/util.rs b/third_party/rust/authenticator/src/util.rs new file mode 100644 index 0000000000..4ccb3c9703 --- /dev/null +++ b/third_party/rust/authenticator/src/util.rs @@ -0,0 +1,67 @@ +/* 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; + +macro_rules! try_or { + ($val:expr, $or:expr) => { + match $val { + Ok(v) => v, + Err(e) => { + return $or(e); + } + } + }; +} + +pub trait Signed { + fn is_negative(&self) -> bool; +} + +impl Signed for i32 { + fn is_negative(&self) -> bool { + *self < (0 as i32) + } +} + +impl Signed for usize { + fn is_negative(&self) -> bool { + (*self as isize) < (0 as isize) + } +} + +#[cfg(any(target_os = "linux"))] +pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> { + if rv.is_negative() { + let errno = unsafe { *libc::__errno_location() }; + Err(io::Error::from_raw_os_error(errno)) + } else { + Ok(rv) + } +} + +#[cfg(any(target_os = "freebsd"))] +pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> { + if rv.is_negative() { + let errno = unsafe { *libc::__error() }; + Err(io::Error::from_raw_os_error(errno)) + } else { + Ok(rv) + } +} + +#[cfg(any(target_os = "openbsd"))] +pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> { + if rv.is_negative() { + Err(io::Error::last_os_error()) + } else { + Ok(rv) + } +} + +pub fn io_err(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::Other, msg) +} diff --git a/third_party/rust/authenticator/src/virtualdevices/mod.rs b/third_party/rust/authenticator/src/virtualdevices/mod.rs new file mode 100644 index 0000000000..5c0a9d39fc --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/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/. */ + +#[cfg(feature = "webdriver")] +pub mod webdriver; + +pub mod software_u2f; diff --git a/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs new file mode 100644 index 0000000000..a88e74de50 --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs @@ -0,0 +1,58 @@ +/* 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 struct SoftwareU2FToken {} + +// This is simply for platforms that aren't using the U2F Token, usually for builds +// without --feature webdriver +#[allow(dead_code)] + +impl SoftwareU2FToken { + pub fn new() -> SoftwareU2FToken { + Self {} + } + + pub fn register( + &self, + _flags: crate::RegisterFlags, + _timeout: u64, + _challenge: Vec<u8>, + _application: crate::AppId, + _key_handles: Vec<crate::KeyHandle>, + ) -> crate::Result<crate::RegisterResult> { + Ok((vec![0u8; 16], self.dev_info())) + } + + /// The implementation of this method must return quickly and should + /// report its status via the status and callback methods + pub fn sign( + &self, + _flags: crate::SignFlags, + _timeout: u64, + _challenge: Vec<u8>, + _app_ids: Vec<crate::AppId>, + _key_handles: Vec<crate::KeyHandle>, + ) -> crate::Result<crate::SignResult> { + Ok((vec![0u8; 0], vec![0u8; 0], vec![0u8; 0], self.dev_info())) + } + + pub fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo { + crate::u2ftypes::U2FDeviceInfo { + vendor_name: b"Mozilla".to_vec(), + device_name: b"Authenticator Webdriver Token".to_vec(), + version_interface: 0, + version_major: 1, + version_minor: 2, + version_build: 3, + cap_flags: 0, + } + } +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests {} diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs new file mode 100644 index 0000000000..b1ef27d813 --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/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/. */ + +mod testtoken; +mod virtualmanager; +mod web_api; + +pub use virtualmanager::VirtualManager; diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs new file mode 100644 index 0000000000..9bf60bbaf5 --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs @@ -0,0 +1,140 @@ +/* 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::virtualdevices::software_u2f::SoftwareU2FToken; +use crate::{RegisterFlags, RegisterResult, SignFlags, SignResult}; + +pub enum TestWireProtocol { + CTAP1, + CTAP2, +} + +impl TestWireProtocol { + pub fn to_webdriver_string(&self) -> String { + match self { + TestWireProtocol::CTAP1 => "ctap1/u2f".to_string(), + TestWireProtocol::CTAP2 => "ctap2".to_string(), + } + } +} + +pub struct TestTokenCredential { + pub credential: Vec<u8>, + pub privkey: Vec<u8>, + pub user_handle: Vec<u8>, + pub sign_count: u64, + pub is_resident_credential: bool, + pub rp_id: String, +} + +pub struct TestToken { + pub id: u64, + pub protocol: TestWireProtocol, + pub transport: String, + pub is_user_consenting: bool, + pub has_user_verification: bool, + pub is_user_verified: bool, + pub has_resident_key: bool, + pub u2f_impl: Option<SoftwareU2FToken>, + pub credentials: Vec<TestTokenCredential>, +} + +impl TestToken { + pub fn new( + id: u64, + protocol: TestWireProtocol, + transport: String, + is_user_consenting: bool, + has_user_verification: bool, + is_user_verified: bool, + has_resident_key: bool, + ) -> TestToken { + match protocol { + TestWireProtocol::CTAP1 => Self { + id, + protocol, + transport, + is_user_consenting, + has_user_verification, + is_user_verified, + has_resident_key, + u2f_impl: Some(SoftwareU2FToken::new()), + credentials: Vec::new(), + }, + _ => unreachable!(), + } + } + + pub fn insert_credential( + &mut self, + credential: &[u8], + privkey: &[u8], + rp_id: String, + is_resident_credential: bool, + user_handle: &[u8], + sign_count: u64, + ) { + let c = TestTokenCredential { + credential: credential.to_vec(), + privkey: privkey.to_vec(), + rp_id, + is_resident_credential, + user_handle: user_handle.to_vec(), + sign_count, + }; + + match self + .credentials + .binary_search_by_key(&credential, |probe| &probe.credential) + { + Ok(_) => {} + Err(idx) => self.credentials.insert(idx, c), + } + } + + pub fn delete_credential(&mut self, credential: &[u8]) -> bool { + debug!("Asking to delete credential",); + if let Ok(idx) = self + .credentials + .binary_search_by_key(&credential, |probe| &probe.credential) + { + debug!("Asking to delete credential from idx {}", idx); + self.credentials.remove(idx); + return true; + } + + false + } + + pub fn register(&self) -> crate::Result<RegisterResult> { + if self.u2f_impl.is_some() { + return self.u2f_impl.as_ref().unwrap().register( + RegisterFlags::empty(), + 10_000, + vec![0; 32], + vec![0; 32], + vec![], + ); + } + Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )) + } + + pub fn sign(&self) -> crate::Result<SignResult> { + if self.u2f_impl.is_some() { + return self.u2f_impl.as_ref().unwrap().sign( + SignFlags::empty(), + 10_000, + vec![0; 32], + vec![vec![0; 32]], + vec![], + ); + } + Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )) + } +} diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs new file mode 100644 index 0000000000..dc26df07ee --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs @@ -0,0 +1,157 @@ +/* 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 runloop::RunLoop; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::ops::Deref; +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; +use std::vec; +use std::{io, string, thread}; + +use crate::authenticatorservice::AuthenticatorTransport; +use crate::errors; +use crate::statecallback::StateCallback; +use crate::virtualdevices::webdriver::{testtoken, web_api}; + +pub struct VirtualManagerState { + pub authenticator_counter: u64, + pub tokens: vec::Vec<testtoken::TestToken>, +} + +impl VirtualManagerState { + pub fn new() -> Arc<Mutex<VirtualManagerState>> { + Arc::new(Mutex::new(VirtualManagerState { + authenticator_counter: 0, + tokens: vec![], + })) + } +} + +pub struct VirtualManager { + addr: SocketAddr, + state: Arc<Mutex<VirtualManagerState>>, + rloop: Option<RunLoop>, +} + +impl VirtualManager { + pub fn new() -> io::Result<Self> { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080); + let state = VirtualManagerState::new(); + let stateclone = state.clone(); + + let builder = thread::Builder::new().name("WebDriver Command Server".into()); + builder.spawn(move || { + web_api::serve(stateclone, addr); + })?; + + Ok(Self { + addr, + state, + rloop: None, + }) + } + + pub fn url(&self) -> string::String { + format!("http://{}/webauthn/authenticator", &self.addr) + } +} + +impl AuthenticatorTransport for VirtualManager { + fn register( + &mut self, + _flags: crate::RegisterFlags, + timeout: u64, + _challenge: Vec<u8>, + _application: crate::AppId, + _key_handles: Vec<crate::KeyHandle>, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + if self.rloop.is_some() { + error!("WebDriver state error, prior operation never cancelled."); + return Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )); + } + + let state = self.state.clone(); + let rloop = try_or!( + RunLoop::new_with_timeout( + move |alive| { + while alive() { + let state_obj = state.lock().unwrap(); + + for token in state_obj.tokens.deref() { + if token.is_user_consenting { + let register_result = token.register(); + thread::spawn(move || { + callback.call(register_result); + }); + return; + } + } + } + }, + timeout + ), + |_| Err(errors::AuthenticatorError::Platform) + ); + + self.rloop = Some(rloop); + Ok(()) + } + + fn sign( + &mut self, + _flags: crate::SignFlags, + timeout: u64, + _challenge: Vec<u8>, + _app_ids: Vec<crate::AppId>, + _key_handles: Vec<crate::KeyHandle>, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + if self.rloop.is_some() { + error!("WebDriver state error, prior operation never cancelled."); + return Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )); + } + + let state = self.state.clone(); + let rloop = try_or!( + RunLoop::new_with_timeout( + move |alive| { + while alive() { + let state_obj = state.lock().unwrap(); + + for token in state_obj.tokens.deref() { + if token.is_user_consenting { + let sign_result = token.sign(); + thread::spawn(move || { + callback.call(sign_result); + }); + return; + } + } + } + }, + timeout + ), + |_| Err(errors::AuthenticatorError::Platform) + ); + + self.rloop = Some(rloop); + Ok(()) + } + + fn cancel(&mut self) -> crate::Result<()> { + if let Some(r) = self.rloop.take() { + debug!("WebDriver operation cancelled."); + r.cancel(); + } + Ok(()) + } +} diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs new file mode 100644 index 0000000000..9093938664 --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs @@ -0,0 +1,965 @@ +/* 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 serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use std::string; +use std::sync::{Arc, Mutex}; +use warp::Filter; + +use crate::virtualdevices::webdriver::{testtoken, virtualmanager::VirtualManagerState}; + +fn default_as_false() -> bool { + false +} +fn default_as_true() -> bool { + false +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct AuthenticatorConfiguration { + protocol: string::String, + transport: string::String, + #[serde(rename = "hasResidentKey")] + #[serde(default = "default_as_false")] + has_resident_key: bool, + #[serde(rename = "hasUserVerification")] + #[serde(default = "default_as_false")] + has_user_verification: bool, + #[serde(rename = "isUserConsenting")] + #[serde(default = "default_as_true")] + is_user_consenting: bool, + #[serde(rename = "isUserVerified")] + #[serde(default = "default_as_false")] + is_user_verified: bool, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct CredentialParameters { + #[serde(rename = "credentialId")] + credential_id: String, + #[serde(rename = "isResidentCredential")] + is_resident_credential: bool, + #[serde(rename = "rpId")] + rp_id: String, + #[serde(rename = "privateKey")] + private_key: String, + #[serde(rename = "userHandle")] + #[serde(default)] + user_handle: String, + #[serde(rename = "signCount")] + sign_count: u64, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct UserVerificationParameters { + #[serde(rename = "isUserVerified")] + is_user_verified: bool, +} + +impl CredentialParameters { + fn new_from_test_token_credential(tc: &testtoken::TestTokenCredential) -> CredentialParameters { + let credential_id = base64::encode_config(&tc.credential, base64::URL_SAFE); + + let private_key = base64::encode_config(&tc.privkey, base64::URL_SAFE); + + let user_handle = base64::encode_config(&tc.user_handle, base64::URL_SAFE); + + CredentialParameters { + credential_id, + is_resident_credential: tc.is_resident_credential, + rp_id: tc.rp_id.clone(), + private_key, + user_handle, + sign_count: tc.sign_count, + } + } +} + +fn with_state( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = (Arc<Mutex<VirtualManagerState>>,), Error = std::convert::Infallible> + Clone +{ + warp::any().map(move || state.clone()) +} + +fn authenticator_add( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator") + .and(warp::post()) + .and(warp::body::json()) + .and(with_state(state)) + .and_then(handlers::authenticator_add) +} + +fn authenticator_delete( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64) + .and(warp::delete()) + .and(with_state(state)) + .and_then(handlers::authenticator_delete) +} + +fn authenticator_set_uv( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "uv") + .and(warp::post()) + .and(warp::body::json()) + .and(with_state(state)) + .and_then(handlers::authenticator_set_uv) +} + +// This is not part of the specification, but it's useful for debugging +fn authenticator_get( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64) + .and(warp::get()) + .and(with_state(state)) + .and_then(handlers::authenticator_get) +} + +fn authenticator_credential_add( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credential") + .and(warp::post()) + .and(warp::body::json()) + .and(with_state(state)) + .and_then(handlers::authenticator_credential_add) +} + +fn authenticator_credential_delete( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credentials" / String) + .and(warp::delete()) + .and(with_state(state)) + .and_then(handlers::authenticator_credential_delete) +} + +fn authenticator_credentials_get( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credentials") + .and(warp::get()) + .and(with_state(state)) + .and_then(handlers::authenticator_credentials_get) +} + +fn authenticator_credentials_clear( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credentials") + .and(warp::delete()) + .and(with_state(state)) + .and_then(handlers::authenticator_credentials_clear) +} + +mod handlers { + use super::{CredentialParameters, UserVerificationParameters}; + use crate::virtualdevices::webdriver::{ + testtoken, virtualmanager::VirtualManagerState, web_api::AuthenticatorConfiguration, + }; + use serde::Serialize; + use std::convert::Infallible; + use std::ops::DerefMut; + use std::sync::{Arc, Mutex}; + use std::vec; + use warp::http::{uri, StatusCode}; + + #[derive(Serialize)] + struct JsonSuccess {} + + impl JsonSuccess { + pub fn blank() -> JsonSuccess { + JsonSuccess {} + } + } + + #[derive(Serialize)] + struct JsonError { + #[serde(skip_serializing_if = "Option::is_none")] + line: Option<u32>, + error: String, + details: String, + } + + impl JsonError { + pub fn new(error: &str, line: u32, details: &str) -> JsonError { + JsonError { + details: details.to_string(), + error: error.to_string(), + line: Some(line), + } + } + pub fn from_status_code(code: StatusCode) -> JsonError { + JsonError { + details: code.canonical_reason().unwrap().to_string(), + line: None, + error: "".to_string(), + } + } + pub fn from_error(error: &str) -> JsonError { + JsonError { + details: "".to_string(), + error: error.to_string(), + line: None, + } + } + } + + macro_rules! reply_error { + ($status:expr) => { + warp::reply::with_status( + warp::reply::json(&JsonError::from_status_code($status)), + $status, + ) + }; + } + + macro_rules! try_json { + ($val:expr, $status:expr) => { + match $val { + Ok(v) => v, + Err(e) => { + return Ok(warp::reply::with_status( + warp::reply::json(&JsonError::new( + $status.canonical_reason().unwrap(), + line!(), + &e.to_string(), + )), + $status, + )); + } + } + }; + } + + pub fn validate_rp_id(rp_id: &str) -> crate::Result<()> { + if let Ok(uri) = rp_id.parse::<uri::Uri>().map_err(|_| { + crate::errors::AuthenticatorError::U2FToken(crate::errors::U2FTokenError::Unknown) + }) { + if uri.scheme().is_none() + && uri.path_and_query().is_none() + && uri.port().is_none() + && uri.host().is_some() + && uri.authority().unwrap() == uri.host().unwrap() + // Don't try too hard to ensure it's a valid domain, just + // ensure there's a label delim in there somewhere + && uri.host().unwrap().find('.').is_some() + { + return Ok(()); + } + } + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + } + + pub async fn authenticator_add( + auth: AuthenticatorConfiguration, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let protocol = match auth.protocol.as_str() { + "ctap1/u2f" => testtoken::TestWireProtocol::CTAP1, + "ctap2" => testtoken::TestWireProtocol::CTAP2, + _ => { + return Ok(warp::reply::with_status( + warp::reply::json(&JsonError::from_error( + format!("unknown protocol: {}", auth.protocol).as_str(), + )), + StatusCode::BAD_REQUEST, + )) + } + }; + + let mut state_lock = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + let mut state_obj = state_lock.deref_mut(); + state_obj.authenticator_counter += 1; + + let tt = testtoken::TestToken::new( + state_obj.authenticator_counter, + protocol, + auth.transport, + auth.is_user_consenting, + auth.has_user_verification, + auth.is_user_verified, + auth.has_resident_key, + ); + + match state_obj + .tokens + .binary_search_by_key(&state_obj.authenticator_counter, |probe| probe.id) + { + Ok(_) => panic!("unexpected repeat of authenticator_id"), + Err(idx) => state_obj.tokens.insert(idx, tt), + } + + #[derive(Serialize)] + struct AddResult { + #[serde(rename = "authenticatorId")] + authenticator_id: u64, + } + + Ok(warp::reply::with_status( + warp::reply::json(&AddResult { + authenticator_id: state_obj.authenticator_counter, + }), + StatusCode::CREATED, + )) + } + + pub async fn authenticator_delete( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + match state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + Ok(idx) => state_obj.tokens.remove(idx), + Err(_) => { + return Ok(reply_error!(StatusCode::NOT_FOUND)); + } + }; + + Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )) + } + + pub async fn authenticator_get( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + + let data = AuthenticatorConfiguration { + protocol: tt.protocol.to_webdriver_string(), + transport: tt.transport.clone(), + has_resident_key: tt.has_resident_key, + has_user_verification: tt.has_user_verification, + is_user_consenting: tt.is_user_consenting, + is_user_verified: tt.is_user_verified, + }; + + return Ok(warp::reply::with_status( + warp::reply::json(&data), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_set_uv( + id: u64, + uv: UserVerificationParameters, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + tt.is_user_verified = uv.is_user_verified; + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credential_add( + id: u64, + auth: CredentialParameters, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let credential = try_json!( + base64::decode_config(&auth.credential_id, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + let privkey = try_json!( + base64::decode_config(&auth.private_key, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + let userhandle = try_json!( + base64::decode_config(&auth.user_handle, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + try_json!(validate_rp_id(&auth.rp_id), StatusCode::BAD_REQUEST); + + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + + tt.insert_credential( + &credential, + &privkey, + auth.rp_id, + auth.is_resident_credential, + &userhandle, + auth.sign_count, + ); + + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::CREATED, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credential_delete( + id: u64, + credential_id: String, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let credential = try_json!( + base64::decode_config(&credential_id, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + + debug!("Asking to delete {}", &credential_id); + + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + debug!("Asking to delete from token {}", tt.id); + if tt.delete_credential(&credential) { + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )); + } + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credentials_get( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + let mut creds: vec::Vec<CredentialParameters> = vec![]; + for ttc in &tt.credentials { + creds.push(CredentialParameters::new_from_test_token_credential(ttc)); + } + + return Ok(warp::reply::with_status( + warp::reply::json(&creds), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credentials_clear( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + + tt.credentials.clear(); + + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } +} + +#[tokio::main] +pub async fn serve(state: Arc<Mutex<VirtualManagerState>>, addr: SocketAddr) { + let routes = authenticator_add(state.clone()) + .or(authenticator_delete(state.clone())) + .or(authenticator_get(state.clone())) + .or(authenticator_set_uv(state.clone())) + .or(authenticator_credential_add(state.clone())) + .or(authenticator_credential_delete(state.clone())) + .or(authenticator_credentials_get(state.clone())) + .or(authenticator_credentials_clear(state.clone())); + + warp::serve(routes).run(addr).await; +} + +#[cfg(test)] +mod tests { + use super::handlers::validate_rp_id; + use super::testtoken::*; + use super::*; + use crate::virtualdevices::webdriver::virtualmanager::VirtualManagerState; + use bytes::Buf; + use std::sync::{Arc, Mutex}; + use warp::http::StatusCode; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[test] + fn test_validate_rp_id() { + init(); + + assert_matches!( + validate_rp_id(&String::from("http://example.com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("https://example.com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("example.com:443")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("example.com/path")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("example.com:443/path")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("user:pass@example.com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!(validate_rp_id(&String::from("example.com")), Ok(())); + } + + fn mk_state_with_token_list(ids: &[u64]) -> Arc<Mutex<VirtualManagerState>> { + let state = VirtualManagerState::new(); + + { + let mut state_obj = state.lock().unwrap(); + for id in ids { + state_obj.tokens.push(TestToken::new( + *id, + TestWireProtocol::CTAP1, + "internal".to_string(), + true, + true, + true, + true, + )); + } + + state_obj.tokens.sort_by_key(|probe| probe.id) + } + + state + } + + fn assert_success_rsp_blank(body: &bytes::Bytes) { + assert_eq!(String::from_utf8_lossy(body.bytes()), r#"{}"#) + } + + fn assert_creds_equals_test_token_params( + a: &[CredentialParameters], + b: &[TestTokenCredential], + ) { + assert_eq!(a.len(), b.len()); + + for (i, j) in a.iter().zip(b.iter()) { + assert_eq!( + i.credential_id, + base64::encode_config(&j.credential, base64::URL_SAFE) + ); + assert_eq!( + i.user_handle, + base64::encode_config(&j.user_handle, base64::URL_SAFE) + ); + assert_eq!( + i.private_key, + base64::encode_config(&j.privkey, base64::URL_SAFE) + ); + assert_eq!(i.rp_id, j.rp_id); + assert_eq!(i.sign_count, j.sign_count); + assert_eq!(i.is_resident_credential, j.is_resident_credential); + } + } + + #[tokio::test] + async fn test_authenticator_add() { + init(); + let filter = authenticator_add(mk_state_with_token_list(&[])); + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + let valid_add = AuthenticatorConfiguration { + protocol: "ctap1/u2f".to_string(), + transport: "usb".to_string(), + has_resident_key: false, + has_user_verification: false, + is_user_consenting: false, + is_user_verified: false, + }; + + { + let mut invalid = valid_add.clone(); + invalid.protocol = "unknown".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + assert!(String::from_utf8_lossy(res.body().bytes()) + .contains(&String::from("unknown protocol: unknown"))); + } + + { + let mut unknown = valid_add.clone(); + unknown.transport = "unknown".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .json(&unknown) + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_eq!( + String::from_utf8_lossy(res.body().bytes()), + r#"{"authenticatorId":1}"# + ) + } + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .json(&valid_add) + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_eq!( + String::from_utf8_lossy(res.body().bytes()), + r#"{"authenticatorId":2}"# + ) + } + } + + #[tokio::test] + async fn test_authenticator_delete() { + init(); + let filter = authenticator_delete(mk_state_with_token_list(&[32])); + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/3") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/32") + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_success_rsp_blank(res.body()); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/42") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + } + + #[tokio::test] + async fn test_authenticator_change_uv() { + init(); + let state = mk_state_with_token_list(&[1]); + let filter = authenticator_set_uv(state.clone()); + + { + let state_obj = state.lock().unwrap(); + assert_eq!(true, state_obj.tokens[0].is_user_verified); + } + + { + // Empty POST is bad + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + // Unexpected POST structure is bad + #[derive(Serialize)] + struct Unexpected { + id: u64, + } + let unexpected = Unexpected { id: 4 }; + + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .json(&unexpected) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let param_false = UserVerificationParameters { + is_user_verified: false, + }; + + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .json(¶m_false) + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(false, state_obj.tokens[0].is_user_verified); + } + + { + let param_false = UserVerificationParameters { + is_user_verified: true, + }; + + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .json(¶m_false) + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(true, state_obj.tokens[0].is_user_verified); + } + } + + #[tokio::test] + async fn test_authenticator_credentials() { + init(); + let state = mk_state_with_token_list(&[1]); + let filter = authenticator_credential_add(state.clone()) + .or(authenticator_credential_delete(state.clone())) + .or(authenticator_credentials_get(state.clone())) + .or(authenticator_credentials_clear(state.clone())); + + let valid_add_credential = CredentialParameters { + credential_id: r"c3VwZXIgcmVhZGVy".to_string(), + is_resident_credential: true, + rp_id: "valid.rpid".to_string(), + private_key: base64::encode_config(b"hello internet~", base64::URL_SAFE), + user_handle: base64::encode_config(b"hello internet~", base64::URL_SAFE), + sign_count: 0, + }; + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let mut invalid = valid_add_credential.clone(); + invalid.credential_id = "!@#$ invalid base64".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let mut invalid = valid_add_credential.clone(); + invalid.rp_id = "example".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let mut invalid = valid_add_credential.clone(); + invalid.rp_id = "https://example.com".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + + let state_obj = state.lock().unwrap(); + assert_eq!(0, state_obj.tokens[0].credentials.len()); + } + + { + let mut no_user_handle = valid_add_credential.clone(); + no_user_handle.user_handle = "".to_string(); + no_user_handle.credential_id = "YQo=".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&no_user_handle) + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(1, state_obj.tokens[0].credentials.len()); + let c = &state_obj.tokens[0].credentials[0]; + assert!(c.user_handle.is_empty()); + } + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&valid_add_credential) + .reply(&filter) + .await; + assert_eq!(res.status(), StatusCode::CREATED); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(2, state_obj.tokens[0].credentials.len()); + let c = &state_obj.tokens[0].credentials[1]; + assert!(!c.user_handle.is_empty()); + } + + { + // Duplicate, should still be two credentials + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&valid_add_credential) + .reply(&filter) + .await; + assert_eq!(res.status(), StatusCode::CREATED); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(2, state_obj.tokens[0].credentials.len()); + } + + { + let res = warp::test::request() + .method("GET") + .path("/webauthn/authenticator/1/credentials") + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + let (_, body) = res.into_parts(); + let cred = serde_json::de::from_slice::<Vec<CredentialParameters>>(&body).unwrap(); + + let state_obj = state.lock().unwrap(); + assert_creds_equals_test_token_params(&cred, &state_obj.tokens[0].credentials); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/1/credentials/YmxhbmsK") + .reply(&filter) + .await; + assert_eq!(res.status(), 404); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/1/credentials/c3VwZXIgcmVhZGVy") + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(1, state_obj.tokens[0].credentials.len()); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/1/credentials") + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(0, state_obj.tokens[0].credentials.len()); + } + } +} diff --git a/third_party/rust/authenticator/src/windows/device.rs b/third_party/rust/authenticator/src/windows/device.rs new file mode 100644 index 0000000000..183ba71f44 --- /dev/null +++ b/third_party/rust/authenticator/src/windows/device.rs @@ -0,0 +1,97 @@ +/* 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::fs::{File, OpenOptions}; +use std::io; +use std::io::{Read, Write}; +use std::os::windows::io::AsRawHandle; + +use super::winapi::DeviceCapabilities; +use crate::consts::{CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; + +#[derive(Debug)] +pub struct Device { + path: String, + file: File, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, +} + +impl Device { + pub fn new(path: String) -> io::Result<Self> { + let file = OpenOptions::new().read(true).write(true).open(&path)?; + Ok(Self { + path, + file, + cid: CID_BROADCAST, + dev_info: None, + }) + } + + pub fn is_u2f(&self) -> bool { + match DeviceCapabilities::new(self.file.as_raw_handle()) { + Ok(caps) => caps.usage() == FIDO_USAGE_U2FHID && caps.usage_page() == FIDO_USAGE_PAGE, + _ => false, + } + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +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() as usize) + } +} + +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<'a>(&'a self) -> &'a [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); + } +} diff --git a/third_party/rust/authenticator/src/windows/mod.rs b/third_party/rust/authenticator/src/windows/mod.rs new file mode 100644 index 0000000000..09135391dd --- /dev/null +++ b/third_party/rust/authenticator/src/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/windows/monitor.rs b/third_party/rust/authenticator/src/windows/monitor.rs new file mode 100644 index 0000000000..4c977bde03 --- /dev/null +++ b/third_party/rust/authenticator/src/windows/monitor.rs @@ -0,0 +1,91 @@ +/* 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::platform::winapi::DeviceInfoSet; +use runloop::RunLoop; +use std::collections::{HashMap, HashSet}; +use std::io; +use std::iter::FromIterator; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +pub struct Monitor<F> +where + F: Fn(String, &dyn Fn() -> bool) + Sync, +{ + runloops: HashMap<String, RunLoop>, + new_device_cb: Arc<F>, +} + +impl<F> Monitor<F> +where + F: Fn(String, &dyn Fn() -> bool) + Send + Sync + 'static, +{ + pub fn new(new_device_cb: F) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> { + let mut stored = HashSet::new(); + + while alive() { + let device_info_set = DeviceInfoSet::new()?; + let devices = HashSet::from_iter(device_info_set.devices()); + + // Remove devices that are gone. + for path in stored.difference(&devices) { + self.remove_device(path); + } + + // Add devices that were plugged in. + for path in devices.difference(&stored) { + self.add_device(path); + } + + // Remember the new set. + stored = devices; + + // 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: &String) { + let f = self.new_device_cb.clone(); + let path = path.clone(); + let key = path.clone(); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: &String) { + 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/windows/transaction.rs b/third_party/rust/authenticator/src/windows/transaction.rs new file mode 100644 index 0000000000..74e856b690 --- /dev/null +++ b/third_party/rust/authenticator/src/windows/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::platform::monitor::Monitor; +use crate::statecallback::StateCallback; +use runloop::RunLoop; + +pub struct Transaction { + // Handle to the thread loop. + thread: Option<RunLoop>, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn(String, &dyn Fn() -> bool) + Sync + Send + 'static, + T: 'static, + { + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb); + + // 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: Some(thread), + }) + } + + pub fn cancel(&mut self) { + // This must never be None. + self.thread.take().unwrap().cancel(); + } +} diff --git a/third_party/rust/authenticator/src/windows/winapi.rs b/third_party/rust/authenticator/src/windows/winapi.rs new file mode 100644 index 0000000000..d3388cebfc --- /dev/null +++ b/third_party/rust/authenticator/src/windows/winapi.rs @@ -0,0 +1,274 @@ +/* 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 crate::platform::winapi::winapi::shared::{guiddef, minwindef, ntdef, windef}; +use crate::platform::winapi::winapi::shared::{hidclass, hidpi, hidusage}; +use crate::platform::winapi::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; +} + +macro_rules! offset_of { + ($ty:ty, $field:ident) => { + unsafe { &(*(0 as *const $ty)).$field as *const _ as usize } + }; +} + +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); + if detail.is_none() { + return None; // malloc() failed. + } + + let detail = detail.unwrap(); + 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 = 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((*self.data).DevicePath.as_ptr(), 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 + } +} |