Path: blob/main/devices/src/usb/backend/fido_backend/fido_device.rs
5394 views
// Copyright 2024 The ChromiumOS Authors1// Use of this source code is governed by a BSD-style license that can be2// found in the LICENSE file.34use std::fs::File;5use std::io::Error as IOError;6use std::io::Write;7use std::sync::Arc;89use base::debug;10use base::error;11use base::warn;12use base::AsRawDescriptor;13use base::EventType;14use base::RawDescriptor;15use sync::Mutex;16use zerocopy::FromBytes;1718use crate::usb::backend::fido_backend::constants;19use crate::usb::backend::fido_backend::error::Error;20use crate::usb::backend::fido_backend::error::Result;21use crate::usb::backend::fido_backend::fido_guest::FidoGuestKey;22use crate::usb::backend::fido_backend::fido_transaction::TransactionManager;23use crate::usb::backend::fido_backend::hid_utils::verify_is_fido_device;24use crate::usb::backend::fido_backend::poll_thread::PollTimer;25use crate::utils::EventLoop;2627#[derive(FromBytes, Debug)]28#[repr(C)]29pub struct InitPacket {30cid: [u8; constants::CID_SIZE],31cmd: u8,32bcnth: u8,33bcntl: u8,34data: [u8; constants::PACKET_INIT_DATA_SIZE],35}3637impl InitPacket {38pub fn extract_cid(bytes: &[u8; constants::U2FHID_PACKET_SIZE]) -> [u8; constants::CID_SIZE] {39// cid is the first 4 bytes. `U2FHID_PACKET_SIZE` > 4, so this cannot fail.40bytes[0..constants::CID_SIZE].try_into().unwrap()41}4243fn is_valid(bytes: &[u8; constants::U2FHID_PACKET_SIZE]) -> bool {44(bytes[4] & constants::PACKET_INIT_VALID_CMD) != 045}4647pub fn from_bytes(bytes: &[u8; constants::U2FHID_PACKET_SIZE]) -> Result<InitPacket> {48if !InitPacket::is_valid(bytes) {49return Err(Error::InvalidInitPacket);50}5152InitPacket::read_from_bytes(bytes).map_err(|_| Error::CannotConvertInitPacketFromBytes)53}5455pub fn bcnt(&self) -> u16 {56(self.bcnth as u16) << 8 | (self.bcntl as u16)57}58}5960/// A virtual representation of a FidoDevice emulated on the Host.61pub struct FidoDevice {62/// Guest representation of the virtual security key device63pub guest_key: Arc<Mutex<FidoGuestKey>>,64/// The `TransactionManager` which handles starting and stopping u2f transactions65pub transaction_manager: Arc<Mutex<TransactionManager>>,66/// Marks whether the current device is active in a transaction. If it is not active, the fd67/// polling event loop does not handle the device fd monitoring.68pub is_active: bool,69/// Marks whether the device has been lost. In case the FD stops being responsive we signal70/// that the device is lost and any further transaction will return a failure.71pub is_device_lost: bool,72/// Backend provider event loop to attach/detach the monitored fd.73event_loop: Arc<EventLoop>,74/// Timer to poll for active USB transfers75pub transfer_timer: PollTimer,76/// fd of the actual hidraw device77pub fd: Arc<Mutex<File>>,78}7980impl AsRawDescriptor for FidoDevice {81fn as_raw_descriptor(&self) -> RawDescriptor {82self.fd.lock().as_raw_descriptor()83}84}8586impl FidoDevice {87pub fn new(hidraw: File, event_loop: Arc<EventLoop>) -> Result<FidoDevice> {88verify_is_fido_device(&hidraw)?;89let timer = PollTimer::new(90"USB transfer timer".to_string(),91std::time::Duration::from_millis(constants::USB_POLL_RATE_MILLIS),92)?;93Ok(FidoDevice {94guest_key: Arc::new(Mutex::new(FidoGuestKey::new()?)),95transaction_manager: Arc::new(Mutex::new(TransactionManager::new()?)),96is_active: false,97is_device_lost: false,98event_loop,99transfer_timer: timer,100fd: Arc::new(Mutex::new(hidraw)),101})102}103104/// Sets the device active state. If the device becomes active, it toggles polling on the file105/// descriptor for the host hid device. If the devices becomes inactive, it stops polling.106/// In case of error, it's not possible to recover so we just log the warning and continue.107pub fn set_active(&mut self, active: bool) {108if self.is_active && !active {109if let Err(e) = self.event_loop.pause_event_for_descriptor(self) {110error!("Could not deactivate polling of host device: {}", e);111}112} else if !self.is_active && active {113if let Err(e) = self114.event_loop115.resume_event_for_descriptor(self, EventType::Read)116{117error!(118"Could not resume polling of host device, transactions will be lost: {}",119e120);121}122}123124self.is_active = active;125}126127/// Starts a new transaction from a given init packet.128pub fn start_transaction(&mut self, packet: &InitPacket) -> Result<()> {129let nonce = if packet.cid == constants::BROADCAST_CID {130packet.data[..constants::NONCE_SIZE]131.try_into()132.map_err(|_| Error::InvalidNonceSize)?133} else {134constants::EMPTY_NONCE135};136137// Start a transaction and the expiration timer if necessary138if self139.transaction_manager140.lock()141.start_transaction(packet.cid, nonce)142{143// Enable the timer that polls for transactions to expire144self.transaction_manager.lock().transaction_timer.arm()?;145}146147// Transition the low level device to active for a response from the host148self.set_active(true);149Ok(())150}151152/// Receives a low-level request from the host device. It means we read data from the actual153/// key on the host.154pub fn recv_from_host(&mut self, packet: &[u8; constants::U2FHID_PACKET_SIZE]) -> Result<()> {155let cid = InitPacket::extract_cid(packet);156let transaction_opt = if cid == constants::BROADCAST_CID {157match InitPacket::from_bytes(packet) {158Ok(packet) => {159// This is a special case, in case of an error message we return to the160// latest broadcast transaction without nonce checking.161if packet.cmd == constants::U2FHID_ERROR_CMD {162self.transaction_manager.lock().get_transaction(cid)163// Otherwise we verify that the nonce matches the right transaction.164} else {165let nonce = packet.data[..constants::NONCE_SIZE]166.try_into()167.map_err(|_| Error::InvalidNonceSize)?;168self.transaction_manager169.lock()170.get_transaction_from_nonce(nonce)171}172}173_ => {174// Drop init transaction with bad init packet175return Ok(());176}177}178} else {179self.transaction_manager.lock().get_transaction(cid)180};181182let transaction = match transaction_opt {183Some(t) => t,184None => {185debug!("Ignoring non-started transaction");186return Ok(());187}188};189190match InitPacket::from_bytes(packet) {191Ok(packet) => {192if packet.cid == constants::BROADCAST_CID {193let nonce = &packet.data[..constants::NONCE_SIZE];194if transaction.nonce != nonce {195// In case of an error command we can let it through, otherwise we drop the196// response.197if packet.cmd != constants::U2FHID_ERROR_CMD {198warn!(199"u2f: received a broadcast transaction with mismatched nonce.\200Ignoring transaction."201);202return Ok(());203}204}205}206self.transaction_manager.lock().update_transaction(207cid,208packet.bcnt(),209constants::PACKET_INIT_DATA_SIZE as u16,210);211}212// It's not an init packet, it means it's a continuation packet213Err(Error::InvalidInitPacket) => {214self.transaction_manager.lock().update_transaction(215cid,216transaction.resp_bcnt,217transaction.resp_size + constants::PACKET_CONT_DATA_SIZE as u16,218);219}220Err(e) => {221error!(222"u2f: received an invalid transaction state: {:?}. Ignoring transaction.",223e224);225return Ok(());226}227}228229// Fetch the transaction again to check if we are done processing it or if we should wait230// for more continuation packets.231let transaction = match self.transaction_manager.lock().get_transaction(cid) {232Some(t) => t,233None => {234error!(235"We lost a transaction on the way. This is a bug. (cid: {:?})",236cid237);238return Ok(());239}240};241// Check for the end of the transaction242if transaction.resp_size >= transaction.resp_bcnt {243if self244.transaction_manager245.lock()246.close_transaction(transaction.cid)247{248// Resets the device as inactive, since we're not waiting for more data to come249// from the host.250self.set_active(false);251}252}253254let mut guest_key = self.guest_key.lock();255if guest_key.pending_in_packets.is_empty() {256// We start polling waiting to send the data back to the guest.257if let Err(e) = guest_key.timer.arm() {258error!(259"Unable to start U2F guest key timer. U2F packets may be lost. {}",260e261);262}263}264guest_key.pending_in_packets.push_back(*packet);265266Ok(())267}268269/// Receives a request from the guest device to write into the actual device on the host.270pub fn recv_from_guest(271&mut self,272packet: &[u8; constants::U2FHID_PACKET_SIZE],273) -> Result<usize> {274// The first byte in the host packet request is the HID report request ID as required by275// the Linux kernel. The real request data starts from the second byte, so we need to276// allocate one extra byte in our write buffer.277// See: https://docs.kernel.org/hid/hidraw.html#write278let mut host_packet = vec![0; constants::U2FHID_PACKET_SIZE + 1];279280match InitPacket::from_bytes(packet) {281Ok(init_packet) => {282self.start_transaction(&init_packet)?;283}284Err(Error::InvalidInitPacket) => {285// It's not an init packet, so we don't start a transaction.286}287Err(e) => {288warn!("Received malformed or invalid u2f-hid init packet, request will be dropped");289return Err(e);290}291}292293host_packet[1..].copy_from_slice(packet.as_slice());294295let written = self296.fd297.lock()298.write(&host_packet)299.map_err(Error::WriteHidrawDevice)?;300301if written != host_packet.len() {302return Err(Error::WriteHidrawDevice(IOError::other(303"Wrote too few bytes to hidraw device.",304)));305}306307// we subtract 1 because we added 1 extra byte to the host packet308Ok(host_packet.len() - 1)309}310}311312313