• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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