use std::collections::VecDeque;
use std::time::Instant;
use base::error;
use base::warn;
cfg_if::cfg_if! {
if #[cfg(test)] {
use base::FakeClock as Clock;
} else {
use base::Clock;
}
}
use crate::usb::backend::fido_backend::constants;
use crate::usb::backend::fido_backend::error::Result;
use crate::usb::backend::fido_backend::poll_thread::PollTimer;
#[derive(Clone, Copy, Debug)]
pub struct FidoTransaction {
pub cid: u32,
pub resp_bcnt: u16,
pub resp_size: u16,
pub nonce: [u8; constants::NONCE_SIZE],
submission_time: Instant,
}
pub struct TransactionManager {
transactions: VecDeque<FidoTransaction>,
last_transaction_time: Instant,
pub transaction_timer: PollTimer,
clock: Clock,
}
impl TransactionManager {
pub fn new() -> Result<TransactionManager> {
let timer = PollTimer::new(
"transaction timer".to_string(),
std::time::Duration::from_millis(constants::TRANSACTION_TIMEOUT_MILLIS / 10),
)?;
let clock = Clock::new();
Ok(TransactionManager {
transactions: VecDeque::new(),
last_transaction_time: clock.now(),
clock,
transaction_timer: timer,
})
}
pub fn pop_transaction(&mut self) -> Option<FidoTransaction> {
self.transactions.pop_front()
}
pub fn close_transaction(&mut self, cid: u32) -> bool {
match self.transactions.iter().position(|t| t.cid == cid) {
Some(index) => {
self.transactions.remove(index);
}
None => {
warn!(
"Tried to close a transaction that does not exist. Silently dropping request."
);
}
};
if self.transactions.is_empty() {
return true;
}
false
}
pub fn start_transaction(&mut self, cid: u32, nonce: [u8; constants::NONCE_SIZE]) -> bool {
let transaction = FidoTransaction {
cid,
resp_bcnt: 0,
resp_size: 0,
nonce,
submission_time: self.clock.now(),
};
if self.transactions.len() >= constants::MAX_TRANSACTIONS {
let _ = self.pop_transaction();
}
self.last_transaction_time = transaction.submission_time;
self.transactions.push_back(transaction);
if self.transactions.len() == 1 {
return true;
}
false
}
pub fn expire_transactions(&mut self) -> bool {
if self.transactions.is_empty() {
return true;
}
if self
.clock
.now()
.duration_since(self.last_transaction_time)
.as_millis()
>= constants::TRANSACTION_TIMEOUT_MILLIS.into()
{
self.reset();
return true;
}
false
}
pub fn reset(&mut self) {
self.transactions = VecDeque::new();
self.last_transaction_time = self.clock.now();
if let Err(e) = self.transaction_timer.clear() {
error!(
"Unable to clear transaction manager timer, silently failing. {}",
e
);
}
}
pub fn update_transaction(&mut self, cid: u32, resp_bcnt: u16, resp_size: u16) {
let index = match self
.transactions
.iter()
.position(|t: &FidoTransaction| t.cid == cid)
{
Some(index) => index,
None => {
warn!(
"No u2f transaction found with (cid {}) in the list. Skipping.",
cid
);
return;
}
};
match self.transactions.get_mut(index) {
Some(t_ref) => {
t_ref.resp_bcnt = resp_bcnt;
t_ref.resp_size = resp_size;
}
None => {
error!(
"A u2f transaction was found at index {} but now is gone. This is a bug.",
index
);
}
};
}
pub fn get_transaction(&mut self, cid: u32) -> Option<FidoTransaction> {
let index = match self
.transactions
.iter()
.position(|t: &FidoTransaction| t.cid == cid)
{
Some(index) => index,
None => {
return None;
}
};
match self.transactions.get(index) {
Some(t_ref) => Some(*t_ref),
None => {
error!(
"A u2f transaction was found at index {} but now is gone. This is a bug.",
index
);
None
}
}
}
pub fn get_transaction_from_nonce(
&mut self,
nonce: [u8; constants::NONCE_SIZE],
) -> Option<FidoTransaction> {
let index =
match self.transactions.iter().position(|t: &FidoTransaction| {
t.cid == constants::BROADCAST_CID && t.nonce == nonce
}) {
Some(index) => index,
None => {
return None;
}
};
match self.transactions.get(index) {
Some(t_ref) => Some(*t_ref),
None => {
error!(
"A u2f transaction was found at index {} but now is gone. This is a bug.",
index
);
None
}
}
}
}
#[cfg(test)]
mod tests {
use crate::usb::backend::fido_backend::constants::EMPTY_NONCE;
use crate::usb::backend::fido_backend::constants::MAX_TRANSACTIONS;
use crate::usb::backend::fido_backend::constants::TRANSACTION_TIMEOUT_MILLIS;
use crate::usb::backend::fido_backend::fido_transaction::TransactionManager;
#[test]
fn test_start_transaction() {
let mut manager = TransactionManager::new().unwrap();
let cid = 1234;
assert!(manager.start_transaction(cid, EMPTY_NONCE));
assert_eq!(manager.transactions.len(), 1);
assert_eq!(manager.last_transaction_time, manager.clock.now());
manager.clock.add_ns(100);
assert!(!manager.start_transaction(cid, EMPTY_NONCE));
assert_eq!(manager.transactions.len(), 2);
assert_eq!(manager.last_transaction_time, manager.clock.now());
manager.reset();
for _ in 0..MAX_TRANSACTIONS + 1 {
manager.start_transaction(cid, EMPTY_NONCE);
}
assert_eq!(manager.transactions.len(), MAX_TRANSACTIONS);
}
#[test]
fn test_pop_transaction() {
let mut manager = TransactionManager::new().unwrap();
let cid1 = 1234;
let cid2 = 5678;
manager.start_transaction(cid1, EMPTY_NONCE);
manager.start_transaction(cid2, EMPTY_NONCE);
let popped_transaction = manager.pop_transaction().unwrap();
assert_eq!(popped_transaction.cid, cid1);
}
#[test]
fn test_close_transaction() {
let mut manager = TransactionManager::new().unwrap();
let cid1 = 1234;
let cid2 = 5678;
manager.start_transaction(cid1, EMPTY_NONCE);
manager.start_transaction(cid2, EMPTY_NONCE);
assert!(!manager.close_transaction(cid2));
assert!(!manager.close_transaction(cid2));
assert_eq!(manager.transactions.len(), 1);
assert!(manager.close_transaction(cid1));
}
#[test]
fn test_update_transaction() {
let mut manager = TransactionManager::new().unwrap();
let cid = 1234;
let bcnt = 17;
let size = 56;
manager.start_transaction(cid, EMPTY_NONCE);
manager.update_transaction(cid, bcnt, size);
let transaction = manager.get_transaction(cid).unwrap();
assert_eq!(transaction.resp_bcnt, bcnt);
assert_eq!(transaction.resp_size, size);
}
#[test]
fn test_expire_transactions() {
let mut manager = TransactionManager::new().unwrap();
let cid = 1234;
assert!(manager.expire_transactions());
manager.start_transaction(cid, EMPTY_NONCE);
assert!(!manager.expire_transactions());
manager
.clock
.add_ns(TRANSACTION_TIMEOUT_MILLIS * 1000000 + 1);
assert!(manager.expire_transactions());
assert_eq!(manager.transactions.len(), 0);
}
}