devices/usb/backend/fido_backend/
fido_transaction.rs

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
5use std::collections::VecDeque;
6use std::time::Instant;
7
8use base::error;
9use base::warn;
10
11cfg_if::cfg_if! {
12    if #[cfg(test)] {
13        use base::FakeClock as Clock;
14    } else {
15        use base::Clock;
16    }
17}
18
19use crate::usb::backend::fido_backend::constants;
20use crate::usb::backend::fido_backend::error::Result;
21use 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)]
25pub 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.
41pub 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
52impl 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
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.
76    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.
96    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).
124    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.
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.
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.
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.
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)]
244mod 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]
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]
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]
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]
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]
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}