1 // Copyright 2024 The ChromiumOS Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 use std::collections::VecDeque; 6 use std::time::Instant; 7 8 use base::error; 9 use base::warn; 10 11 cfg_if::cfg_if! { 12 if #[cfg(test)] { 13 use base::FakeClock as Clock; 14 } else { 15 use base::Clock; 16 } 17 } 18 19 use crate::usb::backend::fido_backend::constants; 20 use crate::usb::backend::fido_backend::error::Result; 21 use crate::usb::backend::fido_backend::poll_thread::PollTimer; 22 23 /// Struct representation of a u2f-hid transaction according to the U2FHID protocol standard. 24 #[derive(Clone, Copy, Debug)] 25 pub struct FidoTransaction { 26 /// Client ID of the transaction 27 pub cid: [u8; constants::CID_SIZE], 28 /// BCNT of the response. 29 pub resp_bcnt: u16, 30 /// Total size of the response. 31 pub resp_size: u16, 32 /// Unique nonce for broadcast transactions. 33 /// The nonce size is 8 bytes, if no nonce is given it's empty 34 pub nonce: [u8; constants::NONCE_SIZE], 35 /// Timestamp of the transaction submission time. 36 submission_time: Instant, 37 } 38 39 /// Struct to keep track of all active transactions. It cycles through them, starts, stops and 40 /// removes outdated ones as they expire. 41 pub struct TransactionManager { 42 /// Sorted (by age) list of transactions. 43 transactions: VecDeque<FidoTransaction>, 44 /// Timestamp of the latest transaction. 45 last_transaction_time: Instant, 46 /// Timer used to poll for expired transactions. 47 pub transaction_timer: PollTimer, 48 /// Clock representation, overridden for testing. 49 clock: Clock, 50 } 51 52 impl TransactionManager { new() -> Result<TransactionManager>53 pub fn new() -> Result<TransactionManager> { 54 let timer = PollTimer::new( 55 "transaction timer".to_string(), 56 // Transactions expire after 120 seconds, polling a tenth of the time 57 // sounds acceptable 58 std::time::Duration::from_millis(constants::TRANSACTION_TIMEOUT_MILLIS / 10), 59 )?; 60 let clock = Clock::new(); 61 Ok(TransactionManager { 62 transactions: VecDeque::new(), 63 last_transaction_time: clock.now(), 64 clock, 65 transaction_timer: timer, 66 }) 67 } 68 pop_transaction(&mut self) -> Option<FidoTransaction>69 pub fn pop_transaction(&mut self) -> Option<FidoTransaction> { 70 self.transactions.pop_front() 71 } 72 73 /// Attempts to close a transaction if it exists. Otherwise it silently drops it. 74 /// It returns true to signal that there's no more transactions active and the device can 75 /// return to an idle state. close_transaction(&mut self, cid: [u8; constants::CID_SIZE]) -> bool76 pub fn close_transaction(&mut self, cid: [u8; constants::CID_SIZE]) -> bool { 77 match self.transactions.iter().position(|t| t.cid == cid) { 78 Some(index) => { 79 self.transactions.remove(index); 80 } 81 None => { 82 warn!( 83 "Tried to close a transaction that does not exist. Silently dropping request." 84 ); 85 } 86 }; 87 88 if self.transactions.is_empty() { 89 return true; 90 } 91 false 92 } 93 94 /// Starts a new transaction in the queue. Returns true if it is the first transaction, 95 /// signaling that the device would have to transition from idle to active state. start_transaction( &mut self, cid: [u8; constants::CID_SIZE], nonce: [u8; constants::NONCE_SIZE], ) -> bool96 pub fn start_transaction( 97 &mut self, 98 cid: [u8; constants::CID_SIZE], 99 nonce: [u8; constants::NONCE_SIZE], 100 ) -> bool { 101 let transaction = FidoTransaction { 102 cid, 103 resp_bcnt: 0, 104 resp_size: 0, 105 nonce, 106 submission_time: self.clock.now(), 107 }; 108 109 // Remove the oldest transaction 110 if self.transactions.len() >= constants::MAX_TRANSACTIONS { 111 let _ = self.pop_transaction(); 112 } 113 self.last_transaction_time = transaction.submission_time; 114 self.transactions.push_back(transaction); 115 if self.transactions.len() == 1 { 116 return true; 117 } 118 false 119 } 120 121 /// Tests the transaction expiration time. If the latest transaction time is beyond the 122 /// acceptable timeout, it removes all transactions and signals to reset the device (returns 123 /// true). expire_transactions(&mut self) -> bool124 pub fn expire_transactions(&mut self) -> bool { 125 // We have no transactions pending, so we can just return true 126 if self.transactions.is_empty() { 127 return true; 128 } 129 130 // The transaction manager resets if transactions took too long. We use duration_since 131 // instead of elapsed so we can work with fake clocks in tests. 132 if self 133 .clock 134 .now() 135 .duration_since(self.last_transaction_time) 136 .as_millis() 137 >= constants::TRANSACTION_TIMEOUT_MILLIS.into() 138 { 139 self.reset(); 140 return true; 141 } 142 false 143 } 144 145 /// Resets the `TransactionManager`, dropping all pending transactions. reset(&mut self)146 pub fn reset(&mut self) { 147 self.transactions = VecDeque::new(); 148 self.last_transaction_time = self.clock.now(); 149 if let Err(e) = self.transaction_timer.clear() { 150 error!( 151 "Unable to clear transaction manager timer, silently failing. {}", 152 e 153 ); 154 } 155 } 156 157 /// Updates the bcnt and size of the first transaction that matches the given CID. update_transaction( &mut self, cid: [u8; constants::CID_SIZE], resp_bcnt: u16, resp_size: u16, )158 pub fn update_transaction( 159 &mut self, 160 cid: [u8; constants::CID_SIZE], 161 resp_bcnt: u16, 162 resp_size: u16, 163 ) { 164 let index = match self 165 .transactions 166 .iter() 167 .position(|t: &FidoTransaction| t.cid == cid) 168 { 169 Some(index) => index, 170 None => { 171 warn!( 172 "No u2f transaction found with (cid {:?}) in the list. Skipping.", 173 cid 174 ); 175 return; 176 } 177 }; 178 match self.transactions.get_mut(index) { 179 Some(t_ref) => { 180 t_ref.resp_bcnt = resp_bcnt; 181 t_ref.resp_size = resp_size; 182 } 183 None => { 184 error!( 185 "A u2f transaction was found at index {} but now is gone. This is a bug.", 186 index 187 ); 188 } 189 }; 190 } 191 192 /// Returns the first transaction that matches the given CID. get_transaction(&mut self, cid: [u8; constants::CID_SIZE]) -> Option<FidoTransaction>193 pub fn get_transaction(&mut self, cid: [u8; constants::CID_SIZE]) -> Option<FidoTransaction> { 194 let index = match self 195 .transactions 196 .iter() 197 .position(|t: &FidoTransaction| t.cid == cid) 198 { 199 Some(index) => index, 200 None => { 201 return None; 202 } 203 }; 204 match self.transactions.get(index) { 205 Some(t_ref) => Some(*t_ref), 206 None => { 207 error!( 208 "A u2f transaction was found at index {} but now is gone. This is a bug.", 209 index 210 ); 211 None 212 } 213 } 214 } 215 216 /// Returns the first broadcast transaction that matches the given nonce. get_transaction_from_nonce( &mut self, nonce: [u8; constants::NONCE_SIZE], ) -> Option<FidoTransaction>217 pub fn get_transaction_from_nonce( 218 &mut self, 219 nonce: [u8; constants::NONCE_SIZE], 220 ) -> Option<FidoTransaction> { 221 let index = 222 match self.transactions.iter().position(|t: &FidoTransaction| { 223 t.cid == constants::BROADCAST_CID && t.nonce == nonce 224 }) { 225 Some(index) => index, 226 None => { 227 return None; 228 } 229 }; 230 match self.transactions.get(index) { 231 Some(t_ref) => Some(*t_ref), 232 None => { 233 error!( 234 "A u2f transaction was found at index {} but now is gone. This is a bug.", 235 index 236 ); 237 None 238 } 239 } 240 } 241 } 242 243 #[cfg(test)] 244 mod tests { 245 246 use crate::usb::backend::fido_backend::constants::EMPTY_NONCE; 247 use crate::usb::backend::fido_backend::constants::MAX_TRANSACTIONS; 248 use crate::usb::backend::fido_backend::constants::TRANSACTION_TIMEOUT_MILLIS; 249 use crate::usb::backend::fido_backend::fido_transaction::TransactionManager; 250 251 #[test] test_start_transaction()252 fn test_start_transaction() { 253 let mut manager = TransactionManager::new().unwrap(); 254 let cid = [0x01, 0x02, 0x03, 0x04]; 255 256 assert!(manager.start_transaction(cid, EMPTY_NONCE)); 257 assert_eq!(manager.transactions.len(), 1); 258 assert_eq!(manager.last_transaction_time, manager.clock.now()); 259 260 manager.clock.add_ns(100); 261 262 assert!(!manager.start_transaction(cid, EMPTY_NONCE)); 263 assert_eq!(manager.transactions.len(), 2); 264 assert_eq!(manager.last_transaction_time, manager.clock.now()); 265 266 manager.reset(); 267 268 // We check that we silently drop old transactions once we go over the MAX_TRANSACTIONS 269 // limit. 270 for _ in 0..MAX_TRANSACTIONS + 1 { 271 manager.start_transaction(cid, EMPTY_NONCE); 272 } 273 274 assert_eq!(manager.transactions.len(), MAX_TRANSACTIONS); 275 } 276 277 #[test] test_pop_transaction()278 fn test_pop_transaction() { 279 let mut manager = TransactionManager::new().unwrap(); 280 let cid1 = [0x01, 0x02, 0x03, 0x04]; 281 let cid2 = [0x05, 0x06, 0x07, 0x08]; 282 283 manager.start_transaction(cid1, EMPTY_NONCE); 284 manager.start_transaction(cid2, EMPTY_NONCE); 285 286 let popped_transaction = manager.pop_transaction().unwrap(); 287 288 assert_eq!(popped_transaction.cid, cid1); 289 } 290 291 #[test] test_close_transaction()292 fn test_close_transaction() { 293 let mut manager = TransactionManager::new().unwrap(); 294 let cid1 = [0x01, 0x02, 0x03, 0x04]; 295 let cid2 = [0x05, 0x06, 0x07, 0x08]; 296 297 manager.start_transaction(cid1, EMPTY_NONCE); 298 manager.start_transaction(cid2, EMPTY_NONCE); 299 300 assert!(!manager.close_transaction(cid2)); 301 // We run this a second time to test it doesn't error out when closing already closed 302 // transactions. 303 assert!(!manager.close_transaction(cid2)); 304 assert_eq!(manager.transactions.len(), 1); 305 assert!(manager.close_transaction(cid1)); 306 } 307 308 #[test] test_update_transaction()309 fn test_update_transaction() { 310 let mut manager = TransactionManager::new().unwrap(); 311 let cid = [0x01, 0x02, 0x03, 0x04]; 312 let bcnt = 17; 313 let size = 56; 314 315 manager.start_transaction(cid, EMPTY_NONCE); 316 manager.update_transaction(cid, bcnt, size); 317 318 let transaction = manager.get_transaction(cid).unwrap(); 319 320 assert_eq!(transaction.resp_bcnt, bcnt); 321 assert_eq!(transaction.resp_size, size); 322 } 323 324 #[test] test_expire_transactions()325 fn test_expire_transactions() { 326 let mut manager = TransactionManager::new().unwrap(); 327 let cid = [0x01, 0x02, 0x03, 0x04]; 328 329 // No transactions, so it defaults to true 330 assert!(manager.expire_transactions()); 331 332 manager.start_transaction(cid, EMPTY_NONCE); 333 assert!(!manager.expire_transactions()); 334 335 // Advance clock beyond expiration time, convert milliseconds to nanoseconds 336 manager 337 .clock 338 .add_ns(TRANSACTION_TIMEOUT_MILLIS * 1000000 + 1); 339 assert!(manager.expire_transactions()); 340 assert_eq!(manager.transactions.len(), 0); 341 } 342 } 343