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: u32, 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: u32) -> bool76 pub fn close_transaction(&mut self, cid: u32) -> 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: u32, nonce: [u8; constants::NONCE_SIZE]) -> bool96 pub fn start_transaction(&mut self, cid: u32, nonce: [u8; constants::NONCE_SIZE]) -> bool { 97 let transaction = FidoTransaction { 98 cid, 99 resp_bcnt: 0, 100 resp_size: 0, 101 nonce, 102 submission_time: self.clock.now(), 103 }; 104 105 // Remove the oldest transaction 106 if self.transactions.len() >= constants::MAX_TRANSACTIONS { 107 let _ = self.pop_transaction(); 108 } 109 self.last_transaction_time = transaction.submission_time; 110 self.transactions.push_back(transaction); 111 if self.transactions.len() == 1 { 112 return true; 113 } 114 false 115 } 116 117 /// Tests the transaction expiration time. If the latest transaction time is beyond the 118 /// acceptable timeout, it removes all transactions and signals to reset the device (returns 119 /// true). expire_transactions(&mut self) -> bool120 pub fn expire_transactions(&mut self) -> bool { 121 // We have no transactions pending, so we can just return true 122 if self.transactions.is_empty() { 123 return true; 124 } 125 126 // The transaction manager resets if transactions took too long. We use duration_since 127 // instead of elapsed so we can work with fake clocks in tests. 128 if self 129 .clock 130 .now() 131 .duration_since(self.last_transaction_time) 132 .as_millis() 133 >= constants::TRANSACTION_TIMEOUT_MILLIS.into() 134 { 135 self.reset(); 136 return true; 137 } 138 false 139 } 140 141 /// Resets the `TransactionManager`, dropping all pending transactions. reset(&mut self)142 pub fn reset(&mut self) { 143 self.transactions = VecDeque::new(); 144 self.last_transaction_time = self.clock.now(); 145 if let Err(e) = self.transaction_timer.clear() { 146 error!( 147 "Unable to clear transaction manager timer, silently failing. {}", 148 e 149 ); 150 } 151 } 152 153 /// Updates the bcnt and size of the first transaction that matches the given CID. update_transaction(&mut self, cid: u32, resp_bcnt: u16, resp_size: u16)154 pub fn update_transaction(&mut self, cid: u32, resp_bcnt: u16, resp_size: u16) { 155 let index = match self 156 .transactions 157 .iter() 158 .position(|t: &FidoTransaction| t.cid == cid) 159 { 160 Some(index) => index, 161 None => { 162 warn!( 163 "No u2f transaction found with (cid {}) in the list. Skipping.", 164 cid 165 ); 166 return; 167 } 168 }; 169 match self.transactions.get_mut(index) { 170 Some(t_ref) => { 171 t_ref.resp_bcnt = resp_bcnt; 172 t_ref.resp_size = resp_size; 173 } 174 None => { 175 error!( 176 "A u2f transaction was found at index {} but now is gone. This is a bug.", 177 index 178 ); 179 } 180 }; 181 } 182 183 /// Returns the first transaction that matches the given CID. get_transaction(&mut self, cid: u32) -> Option<FidoTransaction>184 pub fn get_transaction(&mut self, cid: u32) -> Option<FidoTransaction> { 185 let index = match self 186 .transactions 187 .iter() 188 .position(|t: &FidoTransaction| t.cid == cid) 189 { 190 Some(index) => index, 191 None => { 192 return None; 193 } 194 }; 195 match self.transactions.get(index) { 196 Some(t_ref) => Some(*t_ref), 197 None => { 198 error!( 199 "A u2f transaction was found at index {} but now is gone. This is a bug.", 200 index 201 ); 202 None 203 } 204 } 205 } 206 207 /// Returns the first broadcast transaction that matches the given nonce. get_transaction_from_nonce( &mut self, nonce: [u8; constants::NONCE_SIZE], ) -> Option<FidoTransaction>208 pub fn get_transaction_from_nonce( 209 &mut self, 210 nonce: [u8; constants::NONCE_SIZE], 211 ) -> Option<FidoTransaction> { 212 let index = 213 match self.transactions.iter().position(|t: &FidoTransaction| { 214 t.cid == constants::BROADCAST_CID && t.nonce == nonce 215 }) { 216 Some(index) => index, 217 None => { 218 return None; 219 } 220 }; 221 match self.transactions.get(index) { 222 Some(t_ref) => Some(*t_ref), 223 None => { 224 error!( 225 "A u2f transaction was found at index {} but now is gone. This is a bug.", 226 index 227 ); 228 None 229 } 230 } 231 } 232 } 233 234 #[cfg(test)] 235 mod tests { 236 237 use crate::usb::backend::fido_backend::constants::EMPTY_NONCE; 238 use crate::usb::backend::fido_backend::constants::MAX_TRANSACTIONS; 239 use crate::usb::backend::fido_backend::constants::TRANSACTION_TIMEOUT_MILLIS; 240 use crate::usb::backend::fido_backend::fido_transaction::TransactionManager; 241 242 #[test] test_start_transaction()243 fn test_start_transaction() { 244 let mut manager = TransactionManager::new().unwrap(); 245 let cid = 1234; 246 247 assert!(manager.start_transaction(cid, EMPTY_NONCE)); 248 assert_eq!(manager.transactions.len(), 1); 249 assert_eq!(manager.last_transaction_time, manager.clock.now()); 250 251 manager.clock.add_ns(100); 252 253 assert!(!manager.start_transaction(cid, EMPTY_NONCE)); 254 assert_eq!(manager.transactions.len(), 2); 255 assert_eq!(manager.last_transaction_time, manager.clock.now()); 256 257 manager.reset(); 258 259 // We check that we silently drop old transactions once we go over the MAX_TRANSACTIONS 260 // limit. 261 for _ in 0..MAX_TRANSACTIONS + 1 { 262 manager.start_transaction(cid, EMPTY_NONCE); 263 } 264 265 assert_eq!(manager.transactions.len(), MAX_TRANSACTIONS); 266 } 267 268 #[test] test_pop_transaction()269 fn test_pop_transaction() { 270 let mut manager = TransactionManager::new().unwrap(); 271 let cid1 = 1234; 272 let cid2 = 5678; 273 274 manager.start_transaction(cid1, EMPTY_NONCE); 275 manager.start_transaction(cid2, EMPTY_NONCE); 276 277 let popped_transaction = manager.pop_transaction().unwrap(); 278 279 assert_eq!(popped_transaction.cid, cid1); 280 } 281 282 #[test] test_close_transaction()283 fn test_close_transaction() { 284 let mut manager = TransactionManager::new().unwrap(); 285 let cid1 = 1234; 286 let cid2 = 5678; 287 288 manager.start_transaction(cid1, EMPTY_NONCE); 289 manager.start_transaction(cid2, EMPTY_NONCE); 290 291 assert!(!manager.close_transaction(cid2)); 292 // We run this a second time to test it doesn't error out when closing already closed 293 // transactions. 294 assert!(!manager.close_transaction(cid2)); 295 assert_eq!(manager.transactions.len(), 1); 296 assert!(manager.close_transaction(cid1)); 297 } 298 299 #[test] test_update_transaction()300 fn test_update_transaction() { 301 let mut manager = TransactionManager::new().unwrap(); 302 let cid = 1234; 303 let bcnt = 17; 304 let size = 56; 305 306 manager.start_transaction(cid, EMPTY_NONCE); 307 manager.update_transaction(cid, bcnt, size); 308 309 let transaction = manager.get_transaction(cid).unwrap(); 310 311 assert_eq!(transaction.resp_bcnt, bcnt); 312 assert_eq!(transaction.resp_size, size); 313 } 314 315 #[test] test_expire_transactions()316 fn test_expire_transactions() { 317 let mut manager = TransactionManager::new().unwrap(); 318 let cid = 1234; 319 320 // No transactions, so it defaults to true 321 assert!(manager.expire_transactions()); 322 323 manager.start_transaction(cid, EMPTY_NONCE); 324 assert!(!manager.expire_transactions()); 325 326 // Advance clock beyond expiration time, convert milliseconds to nanoseconds 327 manager 328 .clock 329 .add_ns(TRANSACTION_TIMEOUT_MILLIS * 1000000 + 1); 330 assert!(manager.expire_transactions()); 331 assert_eq!(manager.transactions.len(), 0); 332 } 333 } 334