Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
google
GitHub Repository: google/crosvm
Path: blob/main/devices/src/usb/backend/fido_backend/fido_transaction.rs
5394 views
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 {
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)]
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]
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
}
343
344