diff --git a/src/lib_ykoath2/mod.rs b/src/lib_ykoath2/mod.rs index b974672..837189d 100644 --- a/src/lib_ykoath2/mod.rs +++ b/src/lib_ykoath2/mod.rs @@ -5,7 +5,7 @@ use transaction::*; /// Utilities for interacting with YubiKey OATH/TOTP functionality extern crate pcsc; use pbkdf2::pbkdf2_hmac_array; -use regex::{Match, Regex}; +use regex::Regex; use sha1::Sha1; use std::str::{self}; @@ -18,22 +18,102 @@ use std::hash::{Hash, Hasher}; use std::time::SystemTime; -pub struct CredentialData<'a> { - pub name: &'a str, +#[derive(Debug)] +pub struct CredentialIDData { + pub name: String, oath_type: OathType, + issuer: Option, + period: u32, +} + +impl CredentialIDData { + pub fn from_tlv(id_bytes: &[u8], oath_type_tag: iso7816_tlv::simple::Tag) -> Self { + let oath_type = if Into::::into(oath_type_tag) == (Tag::Hotp as u8) { + OathType::Hotp + } else { + OathType::Totp + }; + let (issuer, name, period) = CredentialIDData::parse_cred_id(id_bytes, oath_type); + return CredentialIDData { + issuer, + name, + period, + oath_type, + }; + } + + pub fn format_cred_id(&self) -> Vec { + let mut cred_id = String::new(); + + if self.oath_type == OathType::Totp && self.period != DEFAULT_PERIOD { + cred_id.push_str(&format!("{}/", self.period)); + } + + if let Some(issuer) = self.issuer.as_deref() { + cred_id.push_str(&format!("{}:", issuer)); + } + + cred_id.push_str(self.name.as_str()); + return cred_id.into_bytes(); // Convert the string to bytes + } + fn format_code(&self, timestamp: u64, truncated: &[u8]) -> OathCode { + let (valid_from, valid_to) = match self.oath_type { + OathType::Totp => { + let time_step = timestamp / (self.period as u64); + let valid_from = time_step * (self.period as u64); + let valid_to = (time_step + 1) * (self.period as u64); + (valid_from, valid_to) + } + OathType::Hotp => (timestamp, 0x7FFFFFFFFFFFFFFF), + }; + + OathCode { + display: OathCodeDisplay::new(truncated[..].try_into().unwrap()), + valid_from, + valid_to, + } + } + + // Function to parse the credential ID + fn parse_cred_id(cred_id: &[u8], oath_type: OathType) -> (Option, String, u32) { + let data = match str::from_utf8(cred_id) { + Ok(d) => d, + Err(_) => return (None, String::new(), 0), // Handle invalid UTF-8 + }; + + if oath_type == OathType::Totp { + Regex::new(r"^((\d+)/)?(([^:]+):)?(.+)$") + .ok() + .and_then(|r| r.captures(&data)) + .map_or((None, data.to_string(), DEFAULT_PERIOD), |caps| { + let period = (&caps.get(2)) + .and_then(|s| s.as_str().parse::().ok()) + .unwrap_or(DEFAULT_PERIOD); + return (Some(caps[4].to_string()), caps[5].to_string(), period); + }) + } else { + return data + .split_once(':') + .map_or((None, data.to_string(), 0), |(i, n)| { + (Some(i.to_string()), n.to_string(), 0) + }); + } + } +} + +pub struct CredentialData { + id_data: CredentialIDData, hash_algorithm: HashAlgo, // secret: bytes, digits: OathDigits, // = DEFAULT_DIGITS, - period: u32, // = DEFAULT_PERIOD, counter: u32, // = DEFAULT_IMF, - issuer: Option<&'a str>, } -impl<'a> CredentialData<'a> { +impl CredentialData { // TODO: parse_uri pub fn get_id(&self) -> Vec { - return _format_cred_id(self.issuer, self.name, self.oath_type, self.period); + return self.id_data.format_cred_id(); } } @@ -48,10 +128,7 @@ pub struct OathCode { pub struct OathCredential<'a> { device_id: &'a str, id: Vec, - issuer: Option, - name: String, - oath_type: OathType, - period: u64, + id_data: CredentialIDData, touch_required: bool, pub code: Option, } @@ -60,7 +137,7 @@ impl<'a> OathCredential<'a> { pub fn display(&self) -> String { format!( "{}: {}", - self.name, + self.id_data.name, self.code .as_ref() .map(OathCodeDisplay::display) @@ -72,19 +149,21 @@ impl<'a> OathCredential<'a> { impl<'a> PartialOrd for OathCredential<'a> { fn partial_cmp(&self, other: &Self) -> Option { let a = ( - self.issuer + self.id_data + .issuer .clone() - .unwrap_or_else(|| self.name.clone()) + .unwrap_or_else(|| self.id_data.name.clone()) .to_lowercase(), - self.name.to_lowercase(), + self.id_data.name.to_lowercase(), ); let b = ( other + .id_data .issuer .clone() - .unwrap_or_else(|| other.name.clone()) + .unwrap_or_else(|| other.id_data.name.clone()) .to_lowercase(), - other.name.to_lowercase(), + other.id_data.name.to_lowercase(), ); Some(a.cmp(&b)) } @@ -103,54 +182,6 @@ impl<'a> Hash for OathCredential<'a> { } } -fn _format_cred_id(issuer: Option<&str>, name: &str, oath_type: OathType, period: u32) -> Vec { - let mut cred_id = String::new(); - - if oath_type == OathType::Totp && period != DEFAULT_PERIOD { - cred_id.push_str(&format!("{}/", period)); - } - - if let Some(issuer) = issuer { - cred_id.push_str(&format!("{}:", issuer)); - } - - cred_id.push_str(name); - return cred_id.into_bytes(); // Convert the string to bytes -} - -// Function to parse the credential ID -fn _parse_cred_id(cred_id: &[u8], oath_type: OathType) -> (Option, String, u64) { - let data = match str::from_utf8(cred_id) { - Ok(d) => d, - Err(_) => return (None, String::new(), 0), // Handle invalid UTF-8 - }; - - if oath_type == OathType::Totp { - Regex::new(r"^((\d+)/)?(([^:]+):)?(.+)$") - .ok() - .and_then(|r| r.captures(&data)) - .map_or((None, data.to_string(), DEFAULT_PERIOD as u64), |caps| { - let period = caps - .get(2) - .as_ref() - .map(Match::as_str) - .and_then(|s| s.parse::().ok()) - .unwrap_or(DEFAULT_PERIOD); - return ( - Some(caps[4].to_string()), - caps[5].to_string(), - period.into(), - ); - }) - } else { - return data - .split_once(':') - .map_or((None, data.to_string(), 0), |(i, n)| { - (Some(i.to_string()), n.to_string(), 0) - }); - } -} - fn _get_device_id(salt: Vec) -> String { let result = HashAlgo::Sha256.get_hash_fun()(salt.leak()); @@ -182,24 +213,6 @@ fn _get_challenge(timestamp: u32, period: u32) -> [u8; 8] { return ((timestamp / period) as u64).to_be_bytes(); } -fn format_code(credential: &OathCredential, timestamp: u64, truncated: &[u8]) -> OathCode { - let (valid_from, valid_to) = match credential.oath_type { - OathType::Totp => { - let time_step = timestamp / credential.period; - let valid_from = time_step * credential.period; - let valid_to = (time_step + 1) * credential.period; - (valid_from, valid_to) - } - OathType::Hotp => (timestamp, 0x7FFFFFFFFFFFFFFF), - }; - - OathCode { - display: OathCodeDisplay::new(truncated[..].try_into().unwrap()), - valid_from, - valid_to, - } -} - pub struct OathSession<'a> { version: &'a [u8], salt: &'a [u8], @@ -265,22 +278,13 @@ impl<'a> OathSession<'a> { let mut key_buffer = Vec::new(); for (cred_id, meta) in TlvZipIter::from_vec(response?) { - // let name = str::from_utf8(&cred_id.value()).unwrap(); - let oath_type = if Into::::into(meta.tag()) == (Tag::Hotp as u8) { - OathType::Hotp - } else { - OathType::Totp - }; let touch = Into::::into(meta.tag()) == (Tag::Touch as u8); // touch only works with totp, this is intended - let (issuer, name, period) = _parse_cred_id(cred_id.value(), oath_type); + let id_data = CredentialIDData::from_tlv(cred_id.value(), meta.tag()); let cred = OathCredential { device_id: &self.name, id: meta.value().to_vec(), - issuer: issuer, - name: name, - period, + id_data, touch_required: touch, - oath_type, code: if Into::::into(meta.tag()) == (Tag::TruncatedResponse as u8) { assert!(meta.value().len() == 5); let display = OathCodeDisplay::new(meta.value()[..].try_into().unwrap());