refactor: use Duration, SystemTime and Range<SystemTime>

This commit is contained in:
Grimmauld 2025-02-13 22:54:41 +01:00
parent 3a4b703a44
commit 7d3600d684
No known key found for this signature in database
3 changed files with 65 additions and 50 deletions

View file

@ -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;

View file

@ -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<u8> {
}
}
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<u8> {
pub struct RefreshableOathCredential<'a> {
pub cred: OathCredential,
pub code: Option<OathCodeDisplay>,
pub valid_from: u64,
pub valid_to: u64,
pub valid_timeframe: Range<SystemTime>,
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<OathCodeDisplay>, 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<SystemTime> {
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,17 +148,20 @@ impl OathSession<'_> {
}
Ok(Self {
version: clone_with_lifetime(
info_map.get(&(Tag::Version as u8)).unwrap_or(&vec![0u8; 0]),
)
version: info_map
.get(&(Tag::Version as u8))
.unwrap_or(&vec![0u8; 0])
.to_owned()
.leak(),
salt: clone_with_lifetime(info_map.get(&(Tag::Name as u8)).unwrap_or(&vec![0u8; 0]))
salt: info_map
.get(&(Tag::Name as u8))
.unwrap_or(&vec![0u8; 0])
.to_owned()
.leak(),
challenge: clone_with_lifetime(
info_map
challenge: info_map
.get(&(Tag::Challenge as u8))
.unwrap_or(&vec![0u8; 0]),
)
.unwrap_or(&vec![0u8; 0])
.to_owned()
.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<SystemTime>) -> [u8; 8] {
(time_to_u64(timestamp.unwrap_or_else(SystemTime::now)) / 30).to_be_bytes()
fn time_challenge(timestamp: Option<SystemTime>, period: Option<Duration>) -> [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()
}

View file

@ -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<String>,
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>, String, u32) {
fn parse_cred_id(cred_id: &[u8], oath_type: OathType) -> (Option<String>, 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::<u32>().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)
})
}
}