From 7d3600d68437e8ea09bc0219d0aaf5048da4b0c0 Mon Sep 17 00:00:00 2001 From: Grimmauld Date: Thu, 13 Feb 2025 22:54:41 +0100 Subject: [PATCH] refactor: use Duration, SystemTime and Range --- src/constants.rs | 4 +- src/lib.rs | 96 +++++++++++++++++++++++----------------- src/oath_credentialid.rs | 15 ++++--- 3 files changed, 65 insertions(+), 50 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 0bb88cb..e31605a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,11 +1,11 @@ -use std::fmt::Display; +use std::{fmt::Display, time::Duration}; use iso7816_tlv::simple::Tlv; use sha1::Digest; pub const INS_SELECT: u8 = 0xa4; pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01]; -pub const DEFAULT_PERIOD: u32 = 30; +pub const DEFAULT_PERIOD: Duration = Duration::from_secs(30); pub const DEFAULT_DIGITS: OathDigits = OathDigits::Six; pub const DEFAULT_IMF: u32 = 0; diff --git a/src/lib.rs b/src/lib.rs index ce35b0e..444b20e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,11 @@ use transaction::*; mod oath_credential; mod oath_credentialid; /// Utilities for interacting with YubiKey OATH/TOTP functionality -use std::{fmt::Display, time::Duration, time::SystemTime}; +use std::{ + fmt::Display, + ops::{Range, RangeInclusive}, + time::{Duration, Instant, SystemTime}, +}; use base64::{engine::general_purpose, Engine as _}; use hmac::{Hmac, Mac}; @@ -40,16 +44,6 @@ fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec { } } -fn _get_challenge(timestamp: u64, period: u64) -> [u8; 8] { - (timestamp / period).to_be_bytes() -} - -fn time_to_u64(timestamp: SystemTime) -> u64 { - timestamp - .duration_since(SystemTime::UNIX_EPOCH) - .as_ref() - .map_or(0, Duration::as_secs) -} pub struct OathSession<'a> { version: &'a [u8], salt: &'a [u8], @@ -66,8 +60,7 @@ fn clone_with_lifetime(data: &[u8]) -> Vec { pub struct RefreshableOathCredential<'a> { pub cred: OathCredential, pub code: Option, - pub valid_from: u64, - pub valid_to: u64, + pub valid_timeframe: Range, refresh_provider: &'a OathSession<'a>, } @@ -86,15 +79,14 @@ impl<'a> RefreshableOathCredential<'a> { RefreshableOathCredential { cred, code: None, - valid_from: 0, - valid_to: 0, + valid_timeframe: SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH, refresh_provider, } } pub fn force_update(&mut self, code: Option, timestamp: SystemTime) { self.code = code; - (self.valid_from, self.valid_to) = + self.valid_timeframe = RefreshableOathCredential::format_validity_time_frame(self, timestamp); } @@ -115,20 +107,30 @@ impl<'a> RefreshableOathCredential<'a> { } pub fn is_valid(&self) -> bool { - let current_time = time_to_u64(SystemTime::now()); - self.valid_from <= current_time && current_time <= self.valid_to + self.valid_timeframe.contains(&SystemTime::now()) } - fn format_validity_time_frame(&self, timestamp: SystemTime) -> (u64, u64) { - let timestamp_seconds = time_to_u64(timestamp); + fn format_validity_time_frame(&self, timestamp: SystemTime) -> Range { match self.cred.id_data.oath_type { OathType::Totp => { - let time_step = timestamp_seconds / (self.cred.id_data.period as u64); - let valid_from = time_step * (self.cred.id_data.period as u64); - let valid_to = (time_step + 1) * (self.cred.id_data.period as u64); - (valid_from, valid_to) + let timestamp_seconds = timestamp + .duration_since(SystemTime::UNIX_EPOCH) + .as_ref() + .map_or(0, Duration::as_secs); + let time_step = timestamp_seconds / (self.cred.id_data.period.as_secs()); + let valid_from = SystemTime::UNIX_EPOCH + .checked_add(self.cred.id_data.period.saturating_mul(time_step as u32)) + .unwrap(); + // time_step * (self.cred.id_data.period.as_secs()); + let valid_to = valid_from.checked_add(self.cred.id_data.period).unwrap(); + valid_from..valid_to + } + OathType::Hotp => { + timestamp + ..SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs(u64::MAX)) + .unwrap() } - OathType::Hotp => (timestamp_seconds, 0x7FFFFFFFFFFFFFFF), } } } @@ -146,18 +148,21 @@ impl OathSession<'_> { } Ok(Self { - version: clone_with_lifetime( - info_map.get(&(Tag::Version as u8)).unwrap_or(&vec![0u8; 0]), - ) - .leak(), - salt: clone_with_lifetime(info_map.get(&(Tag::Name as u8)).unwrap_or(&vec![0u8; 0])) + version: info_map + .get(&(Tag::Version as u8)) + .unwrap_or(&vec![0u8; 0]) + .to_owned() + .leak(), + salt: info_map + .get(&(Tag::Name as u8)) + .unwrap_or(&vec![0u8; 0]) + .to_owned() + .leak(), + challenge: info_map + .get(&(Tag::Challenge as u8)) + .unwrap_or(&vec![0u8; 0]) + .to_owned() .leak(), - challenge: clone_with_lifetime( - info_map - .get(&(Tag::Challenge as u8)) - .unwrap_or(&vec![0u8; 0]), - ) - .leak(), name: name.to_string(), transaction_context, }) @@ -202,13 +207,13 @@ impl OathSession<'_> { return Err(Error::DeviceMismatch); } - let timestamp = time_to_u64(timestamp_sys.unwrap_or_else(SystemTime::now)); + let timestamp = timestamp_sys.unwrap_or_else(SystemTime::now); let mut data = cred.id_data.as_tlv(); if cred.id_data.oath_type == OathType::Totp { data.extend(to_tlv( Tag::Challenge, - &_get_challenge(timestamp, cred.id_data.period as u64), + &time_challenge(Some(timestamp), Some(cred.id_data.period)), )); } @@ -239,7 +244,10 @@ impl OathSession<'_> { Instruction::CalculateAll as u8, 0, 0x01, - Some(&to_tlv(Tag::Challenge, &time_challenge(Some(timestamp)))), + Some(&to_tlv( + Tag::Challenge, + &time_challenge(Some(timestamp), None), + )), ); let mut key_buffer = Vec::new(); @@ -286,6 +294,12 @@ impl OathSession<'_> { } } -fn time_challenge(timestamp: Option) -> [u8; 8] { - (time_to_u64(timestamp.unwrap_or_else(SystemTime::now)) / 30).to_be_bytes() +fn time_challenge(timestamp: Option, period: Option) -> [u8; 8] { + (timestamp + .unwrap_or_else(SystemTime::now) + .duration_since(SystemTime::UNIX_EPOCH) + .as_ref() + .map_or(0, Duration::as_secs) + / period.unwrap_or(DEFAULT_PERIOD).as_secs()) + .to_be_bytes() } diff --git a/src/oath_credentialid.rs b/src/oath_credentialid.rs index fded71c..71816a5 100644 --- a/src/oath_credentialid.rs +++ b/src/oath_credentialid.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, time::Duration}; use regex::Regex; @@ -9,7 +9,7 @@ pub struct CredentialIDData { pub name: String, pub oath_type: OathType, pub issuer: Option, - pub period: u32, + pub period: Duration, } impl Display for CredentialIDData { @@ -34,7 +34,7 @@ impl CredentialIDData { let mut cred_id = String::new(); if self.oath_type == OathType::Totp && self.period != DEFAULT_PERIOD { - cred_id.push_str(&format!("{}/", self.period)); + cred_id.push_str(&format!("{}/", self.period.as_secs())); } if let Some(issuer) = self.issuer.as_deref() { @@ -46,10 +46,10 @@ impl CredentialIDData { } // Function to parse the credential ID - fn parse_cred_id(cred_id: &[u8], oath_type: OathType) -> (Option, String, u32) { + fn parse_cred_id(cred_id: &[u8], oath_type: OathType) -> (Option, String, Duration) { let data = match std::str::from_utf8(cred_id) { Ok(d) => d, - Err(_) => return (None, String::new(), 0), // Handle invalid UTF-8 + Err(_) => return (None, String::new(), Duration::ZERO), // Handle invalid UTF-8 }; if oath_type == OathType::Totp { @@ -60,13 +60,14 @@ impl CredentialIDData { let period = caps .get(2) .and_then(|s| s.as_str().parse::().ok()) + .map(|t| Duration::from_secs(t as u64)) .unwrap_or(DEFAULT_PERIOD); (Some(caps[4].to_string()), caps[5].to_string(), period) }) } else { data.split_once(':') - .map_or((None, data.to_string(), 0), |(i, n)| { - (Some(i.to_string()), n.to_string(), 0) + .map_or((None, data.to_string(), Duration::ZERO), |(i, n)| { + (Some(i.to_string()), n.to_string(), Duration::ZERO) }) } }