mirror of
https://github.com/LordGrimmauld/yubi-oath-rs.git
synced 2025-03-04 05:44:40 +01:00
fix: oath type parsing mismatches
doc: oath credential id
This commit is contained in:
parent
b7b4e79c14
commit
43dcec3625
5 changed files with 80 additions and 33 deletions
|
@ -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
|
||||||
|
|
19
src/lib.rs
19
src/lib.rs
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue