diff --git a/Cargo.lock b/Cargo.lock index 10900a9..0f4769b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,23 +126,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "lib_ykoath2" -version = "0.1.0" -dependencies = [ - "apdu-core", - "base64", - "getrandom", - "hmac", - "iso7816-tlv", - "ouroboros", - "pbkdf2", - "pcsc", - "regex", - "sha1", - "sha2", -] - [[package]] name = "libc" version = "0.2.169" @@ -431,3 +414,20 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "ykoath2" +version = "0.1.0" +dependencies = [ + "apdu-core", + "base64", + "getrandom", + "hmac", + "iso7816-tlv", + "ouroboros", + "pbkdf2", + "pcsc", + "regex", + "sha1", + "sha2", +] diff --git a/Cargo.toml b/Cargo.toml index 0e45df8..6965597 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ name = "example" path = "./src/example.rs" [package] -name = "lib_ykoath2" +name = "ykoath2" version = "0.1.0" edition = "2021" authors = ["Grimmauld "] diff --git a/src/example.rs b/src/example.rs index 23a59a7..79297b7 100644 --- a/src/example.rs +++ b/src/example.rs @@ -1,9 +1,9 @@ // SPDX-License-Identifier: BSD-3-Clause -use lib_ykoath2::constants::{HashAlgo, OathDigits, OathType, DEFAULT_PERIOD}; -use lib_ykoath2::oath_credential::OathCredential; -use lib_ykoath2::oath_credentialid::CredentialIDData; -use lib_ykoath2::OathSession; +use ykoath2::constants::{HashAlgo, OathDigits, OathType, DEFAULT_PERIOD}; +use ykoath2::oath_credential::OathCredential; +use ykoath2::oath_credentialid::CredentialIDData; +use ykoath2::OathSession; // use crate::args::Cli; // use clap::Parser; @@ -35,11 +35,11 @@ fn main() { println!("Found device with label {}", device_label); let mut session = OathSession::new(yubikey).unwrap(); - // session.set_key(&session.derive_key("1234")).unwrap(); - // session.unlock_session(&session.derive_key("1234")).unwrap(); - // session.unset_key().unwrap(); + /* session.set_key(&session.derive_key("1234")).unwrap(); + session.unlock_session(&session.derive_key("1234")).unwrap(); + session.unset_key().unwrap(); - /* let cred = OathCredential { + let cred = OathCredential { device_id: session.name.clone(), id_data: CredentialIDData { name: "test_cred".to_string(), diff --git a/src/lib.rs b/src/lib.rs index a49775e..ef4d960 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,38 +1,26 @@ -#![allow(unused)] pub mod constants; -use constants::*; -mod transaction; -use transaction::*; pub mod oath_credential; pub mod oath_credentialid; -/// Utilities for interacting with YubiKey OATH/TOTP functionality -use std::{ - fmt::Display, - ops::{Range, RangeInclusive}, - time::{Duration, Instant, SystemTime}, -}; +mod refreshable_oath_credential; +mod transaction; -use base64::{engine::general_purpose, Engine as _}; -use hmac::{Hmac, Mac}; +use constants::*; use oath_credential::*; use oath_credentialid::*; +use refreshable_oath_credential::*; +use transaction::*; -fn _get_device_id(salt: Vec) -> String { - let result = HashAlgo::Sha256.get_hash_fun()(salt.leak()); +use std::time::{Duration, SystemTime}; - // Get the first 16 bytes of the hash - let hash_16_bytes = &result[..16]; +use hmac::{Hmac, Mac}; - // Base64 encode the result and remove padding ('=') - general_purpose::URL_SAFE_NO_PAD.encode(hash_16_bytes) -} -fn _hmac_sha1(key: &[u8], message: &[u8]) -> Vec { +fn hmac_sha1(key: &[u8], message: &[u8]) -> Vec { let mut mac = Hmac::::new_from_slice(key).expect("Invalid key length"); mac.update(message); mac.finalize().into_bytes().to_vec() } -fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec { +fn hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec { if key.len() > algo.digest_size() { algo.get_hash_fun()(key) } else { @@ -40,81 +28,14 @@ fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec { } } -pub struct RefreshableOathCredential<'a> { - pub cred: OathCredential, - pub code: Option, - pub valid_timeframe: Range, - refresh_provider: &'a OathSession, -} - -impl Display for RefreshableOathCredential<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(c) = self.code { - f.write_fmt(format_args!("{}: {}", self.cred.id_data, c)) - } else { - f.write_fmt(format_args!("{}", self.cred.id_data)) - } - } -} - -impl<'a> RefreshableOathCredential<'a> { - pub fn new(cred: OathCredential, refresh_provider: &'a OathSession) -> Self { - RefreshableOathCredential { - cred, - code: None, - valid_timeframe: SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH, - refresh_provider, - } - } - - pub fn force_update(&mut self, code: Option, timestamp: SystemTime) { - self.code = code; - self.valid_timeframe = - RefreshableOathCredential::format_validity_time_frame(self, timestamp); - } - - pub fn refresh(&mut self) { - let timestamp = SystemTime::now(); - let refresh_result = self - .refresh_provider - .calculate_code(&self.cred, Some(timestamp)) - .ok(); - self.force_update(refresh_result, timestamp); - } - - pub fn get_or_refresh(mut self) -> RefreshableOathCredential<'a> { - if !self.is_valid() { - self.refresh(); - } - self - } - - pub fn is_valid(&self) -> bool { - self.valid_timeframe.contains(&SystemTime::now()) - } - - fn format_validity_time_frame(&self, timestamp: SystemTime) -> Range { - match self.cred.id_data.oath_type { - OathType::Totp => { - 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(); - 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() - } - } - } +fn time_challenge(timestamp: Option, period: Option) -> [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() } pub struct OathSession { @@ -180,7 +101,7 @@ impl OathSession { return Ok(()); } - let hmac = _hmac_sha1(key, &chal); + let hmac = hmac_sha1(key, &chal); let random_chal = getrandom::u64()?.to_be_bytes(); let data = &[ to_tlv(Tag::Response, &hmac), @@ -190,7 +111,7 @@ impl OathSession { let resp = self.transaction_context .apdu(0, Instruction::Validate as u8, 0, 0, Some(data))?; - let verification = _hmac_sha1(key, &random_chal); + let verification = hmac_sha1(key, &random_chal); if tlv_to_map(resp.buf) .get(&(Tag::Response as u8)) .map(|v| *v == verification) @@ -208,7 +129,7 @@ impl OathSession { return Err(Error::Authentication); } let random_chal = getrandom::u64()?.to_be_bytes(); - let hmac = _hmac_sha1(key, &random_chal); + let hmac = hmac_sha1(key, &random_chal); let data = &[ to_tlv( Tag::Key, @@ -294,8 +215,7 @@ impl OathSession { return Err(Error::Authentication); } - let cred_id = cred.id_data.format_cred_id(); - let secret_short = _hmac_shorten_key(secret, algo); + let secret_short = hmac_shorten_key(secret, algo); let mut secret_padded = [0u8; HMAC_MINIMUM_KEY_SIZE]; let len_to_copy = secret_short.len().min(HMAC_MINIMUM_KEY_SIZE); // Avoid copying more than 14 secret_padded[(HMAC_MINIMUM_KEY_SIZE - len_to_copy)..] @@ -449,13 +369,3 @@ impl OathSession { Ok(key_buffer) } } - -fn time_challenge(timestamp: Option, period: Option) -> [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() -} diff --git a/src/refreshable_oath_credential.rs b/src/refreshable_oath_credential.rs new file mode 100644 index 0000000..3cfe33c --- /dev/null +++ b/src/refreshable_oath_credential.rs @@ -0,0 +1,84 @@ +use crate::{OathCodeDisplay, OathCredential, OathSession, OathType}; + +use std::{ + fmt::Display, + ops::Range, + time::{Duration, SystemTime}, +}; + +pub struct RefreshableOathCredential<'a> { + pub cred: OathCredential, + pub code: Option, + pub valid_timeframe: Range, + refresh_provider: &'a OathSession, +} + +impl Display for RefreshableOathCredential<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(c) = self.code { + f.write_fmt(format_args!("{}: {}", self.cred.id_data, c)) + } else { + f.write_fmt(format_args!("{}", self.cred.id_data)) + } + } +} + +impl<'a> RefreshableOathCredential<'a> { + pub fn new(cred: OathCredential, refresh_provider: &'a OathSession) -> Self { + RefreshableOathCredential { + cred, + code: None, + valid_timeframe: SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH, + refresh_provider, + } + } + + pub fn force_update(&mut self, code: Option, timestamp: SystemTime) { + self.code = code; + self.valid_timeframe = + RefreshableOathCredential::format_validity_time_frame(self, timestamp); + } + + pub fn refresh(&mut self) { + let timestamp = SystemTime::now(); + let refresh_result = self + .refresh_provider + .calculate_code(&self.cred, Some(timestamp)) + .ok(); + self.force_update(refresh_result, timestamp); + } + + pub fn get_or_refresh(mut self) -> RefreshableOathCredential<'a> { + if !self.is_valid() { + self.refresh(); + } + self + } + + pub fn is_valid(&self) -> bool { + self.valid_timeframe.contains(&SystemTime::now()) + } + + fn format_validity_time_frame(&self, timestamp: SystemTime) -> Range { + match self.cred.id_data.oath_type { + OathType::Totp => { + 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(); + 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() + } + } + } +} diff --git a/src/transaction.rs b/src/transaction.rs index 414dd3b..ebcfc2b 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -77,6 +77,8 @@ impl std::error::Error for Error {} pub struct ApduResponse { pub buf: Vec, pub sw1: u8, + + #[allow(dead_code)] pub sw2: u8, } @@ -213,15 +215,8 @@ pub struct TlvZipIter<'a> { } impl<'a> TlvZipIter<'a> { - pub fn new(value: &'a [u8]) -> Self { - TlvZipIter { - iter: TlvIter::new(value).into_iter(), - } - } pub fn from_vec(value: Vec) -> Self { - TlvZipIter { - iter: TlvIter::from_vec(value).into_iter(), - } + Self::from_tlv_iter(TlvIter::from_vec(value).into_iter()) } pub fn from_tlv_iter(value: TlvIter<'a>) -> Self { @@ -246,7 +241,7 @@ impl<'a> TlvIter<'a> { TlvIter { buf: value } } pub fn from_vec(value: Vec) -> Self { - TlvIter { buf: value.leak() } + TlvIter::new(value.leak()) } } @@ -262,14 +257,3 @@ impl Iterator for TlvIter<'_> { r.ok() } } - -pub fn tlv_to_lists(data: Vec) -> HashMap>> { - let mut parsed_manual: HashMap>> = HashMap::new(); - for res in TlvIter::from_vec(data) { - parsed_manual - .entry(res.tag().into()) - .or_default() - .push(res.value().to_vec()); - } - parsed_manual -}