Path: blob/main/devices/src/usb/backend/fido_backend/fido_transaction.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::collections::VecDeque;5use std::time::Instant;67use base::error;8use base::warn;910cfg_if::cfg_if! {11if #[cfg(test)] {12use base::FakeClock as Clock;13} else {14use base::Clock;15}16}1718use crate::usb::backend::fido_backend::constants;19use crate::usb::backend::fido_backend::error::Result;20use crate::usb::backend::fido_backend::poll_thread::PollTimer;2122/// Struct representation of a u2f-hid transaction according to the U2FHID protocol standard.23#[derive(Clone, Copy, Debug)]24pub struct FidoTransaction {25/// Client ID of the transaction26pub cid: [u8; constants::CID_SIZE],27/// BCNT of the response.28pub resp_bcnt: u16,29/// Total size of the response.30pub resp_size: u16,31/// Unique nonce for broadcast transactions.32/// The nonce size is 8 bytes, if no nonce is given it's empty33pub nonce: [u8; constants::NONCE_SIZE],34/// Timestamp of the transaction submission time.35submission_time: Instant,36}3738/// Struct to keep track of all active transactions. It cycles through them, starts, stops and39/// removes outdated ones as they expire.40pub struct TransactionManager {41/// Sorted (by age) list of transactions.42transactions: VecDeque<FidoTransaction>,43/// Timestamp of the latest transaction.44last_transaction_time: Instant,45/// Timer used to poll for expired transactions.46pub transaction_timer: PollTimer,47/// Clock representation, overridden for testing.48clock: Clock,49}5051impl TransactionManager {52pub fn new() -> Result<TransactionManager> {53let timer = PollTimer::new(54"transaction timer".to_string(),55// Transactions expire after 120 seconds, polling a tenth of the time56// sounds acceptable57std::time::Duration::from_millis(constants::TRANSACTION_TIMEOUT_MILLIS / 10),58)?;59let clock = Clock::new();60Ok(TransactionManager {61transactions: VecDeque::new(),62last_transaction_time: clock.now(),63clock,64transaction_timer: timer,65})66}6768pub fn pop_transaction(&mut self) -> Option<FidoTransaction> {69self.transactions.pop_front()70}7172/// Attempts to close a transaction if it exists. Otherwise it silently drops it.73/// It returns true to signal that there's no more transactions active and the device can74/// return to an idle state.75pub fn close_transaction(&mut self, cid: [u8; constants::CID_SIZE]) -> bool {76match self.transactions.iter().position(|t| t.cid == cid) {77Some(index) => {78self.transactions.remove(index);79}80None => {81warn!(82"Tried to close a transaction that does not exist. Silently dropping request."83);84}85};8687if self.transactions.is_empty() {88return true;89}90false91}9293/// Starts a new transaction in the queue. Returns true if it is the first transaction,94/// signaling that the device would have to transition from idle to active state.95pub fn start_transaction(96&mut self,97cid: [u8; constants::CID_SIZE],98nonce: [u8; constants::NONCE_SIZE],99) -> bool {100let transaction = FidoTransaction {101cid,102resp_bcnt: 0,103resp_size: 0,104nonce,105submission_time: self.clock.now(),106};107108// Remove the oldest transaction109if self.transactions.len() >= constants::MAX_TRANSACTIONS {110let _ = self.pop_transaction();111}112self.last_transaction_time = transaction.submission_time;113self.transactions.push_back(transaction);114if self.transactions.len() == 1 {115return true;116}117false118}119120/// Tests the transaction expiration time. If the latest transaction time is beyond the121/// acceptable timeout, it removes all transactions and signals to reset the device (returns122/// true).123pub fn expire_transactions(&mut self) -> bool {124// We have no transactions pending, so we can just return true125if self.transactions.is_empty() {126return true;127}128129// The transaction manager resets if transactions took too long. We use duration_since130// instead of elapsed so we can work with fake clocks in tests.131if self132.clock133.now()134.duration_since(self.last_transaction_time)135.as_millis()136>= constants::TRANSACTION_TIMEOUT_MILLIS.into()137{138self.reset();139return true;140}141false142}143144/// Resets the `TransactionManager`, dropping all pending transactions.145pub fn reset(&mut self) {146self.transactions = VecDeque::new();147self.last_transaction_time = self.clock.now();148if let Err(e) = self.transaction_timer.clear() {149error!(150"Unable to clear transaction manager timer, silently failing. {}",151e152);153}154}155156/// Updates the bcnt and size of the first transaction that matches the given CID.157pub fn update_transaction(158&mut self,159cid: [u8; constants::CID_SIZE],160resp_bcnt: u16,161resp_size: u16,162) {163let index = match self164.transactions165.iter()166.position(|t: &FidoTransaction| t.cid == cid)167{168Some(index) => index,169None => {170warn!(171"No u2f transaction found with (cid {:?}) in the list. Skipping.",172cid173);174return;175}176};177match self.transactions.get_mut(index) {178Some(t_ref) => {179t_ref.resp_bcnt = resp_bcnt;180t_ref.resp_size = resp_size;181}182None => {183error!(184"A u2f transaction was found at index {} but now is gone. This is a bug.",185index186);187}188};189}190191/// Returns the first transaction that matches the given CID.192pub fn get_transaction(&mut self, cid: [u8; constants::CID_SIZE]) -> Option<FidoTransaction> {193let index = match self194.transactions195.iter()196.position(|t: &FidoTransaction| t.cid == cid)197{198Some(index) => index,199None => {200return None;201}202};203match self.transactions.get(index) {204Some(t_ref) => Some(*t_ref),205None => {206error!(207"A u2f transaction was found at index {} but now is gone. This is a bug.",208index209);210None211}212}213}214215/// Returns the first broadcast transaction that matches the given nonce.216pub fn get_transaction_from_nonce(217&mut self,218nonce: [u8; constants::NONCE_SIZE],219) -> Option<FidoTransaction> {220let index =221match self.transactions.iter().position(|t: &FidoTransaction| {222t.cid == constants::BROADCAST_CID && t.nonce == nonce223}) {224Some(index) => index,225None => {226return None;227}228};229match self.transactions.get(index) {230Some(t_ref) => Some(*t_ref),231None => {232error!(233"A u2f transaction was found at index {} but now is gone. This is a bug.",234index235);236None237}238}239}240}241242#[cfg(test)]243mod tests {244245use crate::usb::backend::fido_backend::constants::EMPTY_NONCE;246use crate::usb::backend::fido_backend::constants::MAX_TRANSACTIONS;247use crate::usb::backend::fido_backend::constants::TRANSACTION_TIMEOUT_MILLIS;248use crate::usb::backend::fido_backend::fido_transaction::TransactionManager;249250#[test]251fn test_start_transaction() {252let mut manager = TransactionManager::new().unwrap();253let cid = [0x01, 0x02, 0x03, 0x04];254255assert!(manager.start_transaction(cid, EMPTY_NONCE));256assert_eq!(manager.transactions.len(), 1);257assert_eq!(manager.last_transaction_time, manager.clock.now());258259manager.clock.add_ns(100);260261assert!(!manager.start_transaction(cid, EMPTY_NONCE));262assert_eq!(manager.transactions.len(), 2);263assert_eq!(manager.last_transaction_time, manager.clock.now());264265manager.reset();266267// We check that we silently drop old transactions once we go over the MAX_TRANSACTIONS268// limit.269for _ in 0..MAX_TRANSACTIONS + 1 {270manager.start_transaction(cid, EMPTY_NONCE);271}272273assert_eq!(manager.transactions.len(), MAX_TRANSACTIONS);274}275276#[test]277fn test_pop_transaction() {278let mut manager = TransactionManager::new().unwrap();279let cid1 = [0x01, 0x02, 0x03, 0x04];280let cid2 = [0x05, 0x06, 0x07, 0x08];281282manager.start_transaction(cid1, EMPTY_NONCE);283manager.start_transaction(cid2, EMPTY_NONCE);284285let popped_transaction = manager.pop_transaction().unwrap();286287assert_eq!(popped_transaction.cid, cid1);288}289290#[test]291fn test_close_transaction() {292let mut manager = TransactionManager::new().unwrap();293let cid1 = [0x01, 0x02, 0x03, 0x04];294let cid2 = [0x05, 0x06, 0x07, 0x08];295296manager.start_transaction(cid1, EMPTY_NONCE);297manager.start_transaction(cid2, EMPTY_NONCE);298299assert!(!manager.close_transaction(cid2));300// We run this a second time to test it doesn't error out when closing already closed301// transactions.302assert!(!manager.close_transaction(cid2));303assert_eq!(manager.transactions.len(), 1);304assert!(manager.close_transaction(cid1));305}306307#[test]308fn test_update_transaction() {309let mut manager = TransactionManager::new().unwrap();310let cid = [0x01, 0x02, 0x03, 0x04];311let bcnt = 17;312let size = 56;313314manager.start_transaction(cid, EMPTY_NONCE);315manager.update_transaction(cid, bcnt, size);316317let transaction = manager.get_transaction(cid).unwrap();318319assert_eq!(transaction.resp_bcnt, bcnt);320assert_eq!(transaction.resp_size, size);321}322323#[test]324fn test_expire_transactions() {325let mut manager = TransactionManager::new().unwrap();326let cid = [0x01, 0x02, 0x03, 0x04];327328// No transactions, so it defaults to true329assert!(manager.expire_transactions());330331manager.start_transaction(cid, EMPTY_NONCE);332assert!(!manager.expire_transactions());333334// Advance clock beyond expiration time, convert milliseconds to nanoseconds335manager336.clock337.add_ns(TRANSACTION_TIMEOUT_MILLIS * 1000000 + 1);338assert!(manager.expire_transactions());339assert_eq!(manager.transactions.len(), 0);340}341}342343344