xref: /aosp_15_r20/external/crosvm/devices/src/usb/backend/fido_backend/fido_transaction.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
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