plit out CredentialIDData

This commit is contained in:
Grimmauld 2025-02-10 21:55:01 +01:00
parent c407fb0e31
commit f63da9e470
No known key found for this signature in database

View file

@ -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<String>,
period: u32,
}
impl CredentialIDData {
pub fn from_tlv(id_bytes: &[u8], oath_type_tag: iso7816_tlv::simple::Tag) -> Self {
let oath_type = if Into::<u8>::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<u8> {
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>, 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::<u32>().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<u8> {
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<u8>,
issuer: Option<String>,
name: String,
oath_type: OathType,
period: u64,
id_data: CredentialIDData,
touch_required: bool,
pub code: Option<OathCodeDisplay>,
}
@ -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<Ordering> {
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<u8> {
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>, 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::<u32>().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<u8>) -> 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::<u8>::into(meta.tag()) == (Tag::Hotp as u8) {
OathType::Hotp
} else {
OathType::Totp
};
let touch = Into::<u8>::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::<u8>::into(meta.tag()) == (Tag::TruncatedResponse as u8) {
assert!(meta.value().len() == 5);
let display = OathCodeDisplay::new(meta.value()[..].try_into().unwrap());