fix: oath type parsing mismatches

doc: oath credential id
This commit is contained in:
Grimmauld 2025-02-16 13:33:00 +01:00
parent b7b4e79c14
commit 43dcec3625
No known key found for this signature in database
5 changed files with 80 additions and 33 deletions

View file

@ -168,8 +168,8 @@ impl HashAlgo {
#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)] #[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)]
#[repr(u8)] #[repr(u8)]
pub enum OathType { pub enum OathType {
Totp = 0x10, Hotp = 0x10,
Hotp = 0x20, Totp = 0x20,
} }
/// describes display information of a code, keeping track of the code and number of digits /// describes display information of a code, keeping track of the code and number of digits

View file

@ -40,13 +40,13 @@ fn hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec<u8> {
} }
} }
fn time_challenge(timestamp: Option<SystemTime>, period: Option<Duration>) -> [u8; 8] { fn time_challenge(timestamp: Option<SystemTime>, period: Duration) -> [u8; 8] {
(timestamp (timestamp
.unwrap_or_else(SystemTime::now) .unwrap_or_else(SystemTime::now)
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.as_ref() .as_ref()
.map_or(0, Duration::as_secs) .map_or(0, Duration::as_secs)
/ period.unwrap_or(DEFAULT_PERIOD).as_secs()) / period.as_secs())
.to_be_bytes() .to_be_bytes()
} }
@ -272,7 +272,7 @@ impl OathSession {
if cred.id_data.oath_type == OathType::Totp { if cred.id_data.oath_type == OathType::Totp {
data.extend(to_tlv( data.extend(to_tlv(
Tag::Challenge, Tag::Challenge,
&time_challenge(Some(timestamp), Some(cred.id_data.period)), &time_challenge(Some(timestamp), cred.id_data.get_period()),
)); ));
} }
@ -305,7 +305,7 @@ impl OathSession {
0x01, 0x01,
Some(&to_tlv( Some(&to_tlv(
Tag::Challenge, Tag::Challenge,
&time_challenge(Some(timestamp), None), &time_challenge(Some(timestamp), DEFAULT_PERIOD),
)), )),
); );
@ -339,10 +339,13 @@ impl OathSession {
let mut key_buffer = Vec::new(); let mut key_buffer = Vec::new();
for cred_id in TlvIter::from_vec(response?) { for cred_id in TlvIter::from_vec(response?) {
let id_data = CredentialIDData::from_bytes( let oath_type = if (cred_id.value()[0] & 0xf0) == (Tag::Hotp as u8) {
&cred_id.value()[1..], OathType::Hotp
*cred_id.value().first().unwrap_or(&0u8) & 0xf0, } else {
); OathType::Totp
};
let id_data = CredentialIDData::from_bytes(&cred_id.value()[1..], oath_type);
key_buffer.push(id_data); key_buffer.push(id_data);
} }

View file

@ -44,6 +44,6 @@ impl PartialEq for OathCredential {
impl Hash for OathCredential { impl Hash for OathCredential {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
self.device_id.hash(state); self.device_id.hash(state);
self.id_data.format_cred_id().hash(state); self.id_data.hash(state);
} }
} }

View file

@ -1,15 +1,31 @@
use std::{fmt::Display, time::Duration}; use std::{
fmt::Display,
hash::{Hash, Hasher},
time::Duration,
};
use regex::Regex; use regex::Regex;
use crate::{to_tlv, OathType, Tag, DEFAULT_PERIOD}; use crate::{to_tlv, OathType, Tag, DEFAULT_PERIOD};
/// holds data on one credential.
/// Acts as a handle to credentials when requesting codes from the YubiKey.
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct CredentialIDData { pub struct CredentialIDData {
/// name of a credential.
/// Typically specifies an account.
pub name: String, pub name: String,
/// One of `OathType::Totp` or `OathType::Hotp`.
/// Specifies the type of OTP used represented by this credential.
pub oath_type: OathType, pub oath_type: OathType,
/// issuer of the credential.
/// Typically specifies the platform.
pub issuer: Option<String>, pub issuer: Option<String>,
pub period: Duration,
/// validity period of each generated code.
period: Option<Duration>,
} }
impl Display for CredentialIDData { impl Display for CredentialIDData {
@ -22,19 +38,35 @@ impl Display for CredentialIDData {
} }
impl CredentialIDData { impl CredentialIDData {
/// reads id data from tlv data
/// `id_bytes` refers to the byte buffer containing issuer, name and period
/// `oath_type_tag` refers to the tlv tag containing the oath type information
pub fn from_tlv(id_bytes: &[u8], oath_type_tag: iso7816_tlv::simple::Tag) -> Self { pub fn from_tlv(id_bytes: &[u8], oath_type_tag: iso7816_tlv::simple::Tag) -> Self {
CredentialIDData::from_bytes(id_bytes, Into::<u8>::into(oath_type_tag)) let oath_type = if Into::<u8>::into(oath_type_tag) == Tag::Hotp as u8 {
OathType::Hotp
} else {
OathType::Totp
};
CredentialIDData::from_bytes(id_bytes, oath_type)
} }
/// Reconstructs the tlv data to refer to this credential on the YubiKey
pub fn as_tlv(&self) -> Vec<u8> { pub fn as_tlv(&self) -> Vec<u8> {
to_tlv(Tag::Name, &self.format_cred_id()) to_tlv(Tag::Name, &self.format_cred_id())
} }
pub fn format_cred_id(&self) -> Vec<u8> { /// Returns the defined period or default
pub fn get_period(&self) -> Duration {
self.period.unwrap_or(DEFAULT_PERIOD)
}
fn format_cred_id(&self) -> Vec<u8> {
let mut cred_id = String::new(); let mut cred_id = String::new();
if self.oath_type == OathType::Totp && self.period != DEFAULT_PERIOD { if self.oath_type == OathType::Totp {
cred_id.push_str(&format!("{}/", self.period.as_secs())); if let Some(p) = self.period {
cred_id.push_str(&format!("{}/", p.as_secs()));
}
} }
if let Some(issuer) = self.issuer.as_deref() { if let Some(issuer) = self.issuer.as_deref() {
@ -45,18 +77,20 @@ impl CredentialIDData {
cred_id.into_bytes() // Convert the string to bytes cred_id.into_bytes() // Convert the string to bytes
} }
// Function to parse the credential ID fn parse_cred_id(
fn parse_cred_id(cred_id: &[u8], oath_type: OathType) -> (Option<String>, String, Duration) { cred_id: &[u8],
oath_type: OathType,
) -> (Option<String>, String, Option<Duration>) {
let data = match std::str::from_utf8(cred_id) { let data = match std::str::from_utf8(cred_id) {
Ok(d) => d, Ok(d) => d,
Err(_) => return (None, String::new(), Duration::ZERO), // Handle invalid UTF-8 Err(_) => return (None, String::new(), Some(Duration::ZERO)), // Handle invalid UTF-8
}; };
if oath_type == OathType::Totp { if oath_type == OathType::Totp {
Regex::new(r"^((\d+)/)?(([^:]+):)?(.+)$") Regex::new(r"^((\d+)/)?(([^:]+):)?(.+)$")
.ok() .ok()
.and_then(|r| r.captures(data)) .and_then(|r| r.captures(data))
.map_or((None, data.to_string(), DEFAULT_PERIOD), |caps| { .map_or((None, data.to_string(), None), |caps| {
let period = caps let period = caps
.get(2) .get(2)
.and_then(|s| s.as_str().parse::<u32>().ok()) .and_then(|s| s.as_str().parse::<u32>().ok())
@ -64,22 +98,19 @@ impl CredentialIDData {
.unwrap_or(DEFAULT_PERIOD); .unwrap_or(DEFAULT_PERIOD);
let issuer = caps.get(4).map(|m| m.as_str().to_string()); let issuer = caps.get(4).map(|m| m.as_str().to_string());
let cred_name = caps.get(5).map_or(data, |m| m.as_str()); let cred_name = caps.get(5).map_or(data, |m| m.as_str());
(issuer, cred_name.to_string(), period) (issuer, cred_name.to_string(), Some(period))
}) })
} else { } else {
data.split_once(':') data.split_once(':')
.map_or((None, data.to_string(), Duration::ZERO), |(i, n)| { .map_or((None, data.to_string(), None), |(i, n)| {
(Some(i.to_string()), n.to_string(), Duration::ZERO) (Some(i.to_string()), n.to_string(), None)
}) })
} }
} }
pub(crate) fn from_bytes(id_bytes: &[u8], tag: u8) -> CredentialIDData { /// parses a credential id from byte buffers
let oath_type = if tag == (Tag::Hotp as u8) { /// `id_bytes` contains information about issuer, name and duration
OathType::Hotp pub fn from_bytes(id_bytes: &[u8], oath_type: OathType) -> CredentialIDData {
} else {
OathType::Totp
};
let (issuer, name, period) = CredentialIDData::parse_cred_id(id_bytes, oath_type); let (issuer, name, period) = CredentialIDData::parse_cred_id(id_bytes, oath_type);
CredentialIDData { CredentialIDData {
issuer, issuer,
@ -89,3 +120,9 @@ impl CredentialIDData {
} }
} }
} }
impl Hash for CredentialIDData {
fn hash<H: Hasher>(&self, state: &mut H) {
self.format_cred_id().hash(state);
}
}

View file

@ -66,11 +66,18 @@ impl<'a> RefreshableOathCredential<'a> {
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.as_ref() .as_ref()
.map_or(0, Duration::as_secs); .map_or(0, Duration::as_secs);
let time_step = timestamp_seconds / (self.cred.id_data.period.as_secs()); let time_step = timestamp_seconds / (self.cred.id_data.get_period().as_secs());
let valid_from = SystemTime::UNIX_EPOCH let valid_from = SystemTime::UNIX_EPOCH
.checked_add(self.cred.id_data.period.saturating_mul(time_step as u32)) .checked_add(
self.cred
.id_data
.get_period()
.saturating_mul(time_step as u32),
)
.unwrap();
let valid_to = valid_from
.checked_add(self.cred.id_data.get_period())
.unwrap(); .unwrap();
let valid_to = valid_from.checked_add(self.cred.id_data.period).unwrap();
valid_from..valid_to valid_from..valid_to
} }
OathType::Hotp => { OathType::Hotp => {