diff --git a/Cargo.lock b/Cargo.lock index 1b241bb..3e80e4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hmac" version = "0.12.1" @@ -140,6 +146,8 @@ dependencies = [ "regex", "sha1", "sha2", + "strum", + "strum_macros", ] [[package]] @@ -159,7 +167,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -262,6 +270,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "sha1" version = "0.10.6" @@ -290,6 +304,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strum" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f" + +[[package]] +name = "strum_macros" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index c2be6ad..5d804f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ pcsc = "2.9.0" regex = "1.11.1" sha1 = "0.10.6" sha2 = "0.10.8" +strum = { version = "0.27.0", features = [ ] } +strum_macros = "0.27.0" [package] name = "oath-rs-experiments" diff --git a/src/lib_ykoath2/constants.rs b/src/lib_ykoath2/constants.rs index a265d12..a18957b 100644 --- a/src/lib_ykoath2/constants.rs +++ b/src/lib_ykoath2/constants.rs @@ -1,7 +1,8 @@ use iso7816_tlv::simple::Tlv; -#[crate_type = "lib"] use sha1::{Digest, Sha1}; use sha2::{Sha256, Sha512}; +use strum::IntoEnumIterator; // 0.17.1 +use strum_macros::EnumIter; // 0.17.1 pub const INS_SELECT: u8 = 0xa4; pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01]; @@ -9,6 +10,7 @@ pub const DEFAULT_PERIOD: u32 = 30; pub const DEFAULT_DIGITS: OathDigits = OathDigits::Six; pub const DEFAULT_IMF: u32 = 0; +#[derive(Debug, EnumIter, Clone, Copy, PartialEq, Eq)] #[repr(u16)] pub enum ErrorResponse { NoSpace = 0x6a84, @@ -20,12 +22,48 @@ pub enum ErrorResponse { NoSuchObject = 0x6984, } +impl ErrorResponse { + pub fn as_string(self) -> String { + match self { + ErrorResponse::NoSpace => "No Space left on device", + ErrorResponse::CommandAborted => "Command aborted", + ErrorResponse::InvalidInstruction => "Invalid instruction", + ErrorResponse::AuthRequired => "Authentication required", + ErrorResponse::WrongSyntax => "Wrong syntax", + ErrorResponse::GenericError => "Generic Error", + ErrorResponse::NoSuchObject => "No such Object", + } + .to_string() + } + + pub fn any_match(code: u16) -> Option { + for resp in ErrorResponse::iter() { + if code == resp as u16 { + return Some(resp); + } + } + None + } +} + +#[derive(Debug, EnumIter, Clone, Copy)] #[repr(u16)] pub enum SuccessResponse { MoreData = 0x61, Okay = 0x9000, } +impl SuccessResponse { + pub fn any_match(code: u16) -> Option { + for resp in SuccessResponse::iter() { + if code == resp as u16 { + return Some(resp); + } + } + None + } +} + #[repr(u8)] pub enum Instruction { Put = 0x01, diff --git a/src/lib_ykoath2/mod.rs b/src/lib_ykoath2/mod.rs index da371c5..008209e 100644 --- a/src/lib_ykoath2/mod.rs +++ b/src/lib_ykoath2/mod.rs @@ -181,9 +181,9 @@ impl<'a> OathSession<'a> { &self, cred: OathCredential, timestamp_sys: Option, - ) -> Result { + ) -> Result { if self.name != cred.device_id { - return Err("device id mismatch: Credential no on key".to_string()); + return Err(FormattableErrorResponse::DeviceMismatchError); } let timestamp = time_to_u64(timestamp_sys.unwrap_or_else(SystemTime::now)); @@ -204,15 +204,22 @@ impl<'a> OathSession<'a> { Some(&data), ); - let meta = TlvIter::from_vec(resp?) - .next() - .ok_or("No credentials to unpack found in response".to_string())?; + let meta = + TlvIter::from_vec(resp?) + .next() + .ok_or(FormattableErrorResponse::ParsingError( + "No credentials to unpack found in response".to_string(), + ))?; - OathCodeDisplay::from_tlv(meta).ok_or("error parsing calculation response".to_string()) + OathCodeDisplay::from_tlv(meta).ok_or(FormattableErrorResponse::ParsingError( + "error parsing calculation response".to_string(), + )) } /// Read the OATH codes from the device - pub fn get_oath_codes(&self) -> Result, String> { + pub fn get_oath_codes( + &self, + ) -> Result, FormattableErrorResponse> { let timestamp = SystemTime::now(); // Request OATH codes from device let response = self.transaction_context.apdu_read_all( diff --git a/src/lib_ykoath2/transaction.rs b/src/lib_ykoath2/transaction.rs index 34000f2..a2bdf45 100644 --- a/src/lib_ykoath2/transaction.rs +++ b/src/lib_ykoath2/transaction.rs @@ -5,6 +5,7 @@ extern crate pcsc; use iso7816_tlv::simple::{Tag as TlvTag, Tlv}; use ouroboros::self_referencing; use std::collections::HashMap; +use std::fmt::Display; use std::str::{self}; use apdu_core::{Command, Response}; @@ -13,6 +14,63 @@ use pcsc::{Card, Transaction}; use std::ffi::CString; +#[derive(PartialEq, Eq, Debug)] +pub enum FormattableErrorResponse { + NoError, + Unknown(String), + Protocol(ErrorResponse), + PcscError(pcsc::Error), + ParsingError(String), + DeviceMismatchError, +} + +impl FormattableErrorResponse { + pub fn from_apdu_response(sw1: u8, sw2: u8) -> FormattableErrorResponse { + let code: u16 = (sw1 as u16 | sw2 as u16) << 8; + if let Some(e) = ErrorResponse::any_match(code) { + return FormattableErrorResponse::Protocol(e); + } + if SuccessResponse::any_match(code) + .or(SuccessResponse::any_match(sw1.into())) + .is_some() + { + return FormattableErrorResponse::NoError; + } + FormattableErrorResponse::Unknown(String::from("Unknown error")) + } + pub fn is_ok(&self) -> bool { + *self == FormattableErrorResponse::NoError + } + pub fn as_opt(self) -> Option { + if self.is_ok() { + None + } else { + Some(self) + } + } + + fn from_transmit(err: pcsc::Error) -> FormattableErrorResponse { + FormattableErrorResponse::PcscError(err) + } + + fn as_string(&self) -> String { + match self { + FormattableErrorResponse::NoError => "ok".to_string(), + FormattableErrorResponse::Unknown(msg) => msg.to_owned(), + FormattableErrorResponse::Protocol(error_response) => error_response.as_string(), + FormattableErrorResponse::PcscError(error) => format!("{}", error), + FormattableErrorResponse::ParsingError(msg) => msg.to_owned(), + FormattableErrorResponse::DeviceMismatchError => "Devices do not match".to_string(), + } + } +} + +impl Display for FormattableErrorResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.as_string()) + } +} + pub struct ApduResponse { pub buf: Vec, pub sw1: u8, @@ -27,7 +85,7 @@ fn apdu( parameter1: u8, parameter2: u8, data: Option<&[u8]>, -) -> Result { +) -> Result { let command = if let Some(data) = data { Command::new_with_payload(class, instruction, parameter1, parameter2, data) } else { @@ -42,14 +100,16 @@ fn apdu( // Write the payload to the device and error if there is a problem let rx_buf = match tx.transmit(&tx_buf, &mut rx_buf) { Ok(slice) => slice, - Err(err) => return Err(format!("{}", err)), + // Err(err) => return Err(format!("{}", err)), + Err(err) => return Err(FormattableErrorResponse::from_transmit(err)), }; let resp = Response::from(rx_buf); - let error_context = to_error_response(resp.trailer.0, resp.trailer.1); + let error_context = + FormattableErrorResponse::from_apdu_response(resp.trailer.0, resp.trailer.1); - if let Some(err) = error_context { - return Err(err); + if !error_context.is_ok() { + return Err(error_context); } Ok(ApduResponse { @@ -66,7 +126,7 @@ fn apdu_read_all( parameter1: u8, parameter2: u8, data: Option<&[u8]>, -) -> Result, String> { +) -> Result, FormattableErrorResponse> { let mut response_buf = Vec::new(); let mut resp = apdu(tx, class, instruction, parameter1, parameter2, data)?; response_buf.extend(resp.buf); @@ -77,31 +137,6 @@ fn apdu_read_all( Ok(response_buf) } -fn to_error_response(sw1: u8, sw2: u8) -> Option { - let code: usize = (sw1 as usize | sw2 as usize) << 8; - - match code { - code if code == ErrorResponse::GenericError as usize => Some(String::from("Generic error")), - code if code == ErrorResponse::NoSpace as usize => Some(String::from("No space on device")), - code if code == ErrorResponse::NoSuchObject as usize => { - Some(String::from("No such object")) - } - code if code == ErrorResponse::CommandAborted as usize => { - Some(String::from("Command was aborted")) - } - code if code == ErrorResponse::AuthRequired as usize => { - Some(String::from("Authentication required")) - } - code if code == ErrorResponse::WrongSyntax as usize => Some(String::from("Wrong syntax")), - code if code == ErrorResponse::InvalidInstruction as usize => { - Some(String::from("Invalid instruction")) - } - code if code == SuccessResponse::Okay as usize => None, - sw1 if sw1 == SuccessResponse::MoreData as usize => None, - _ => Some(String::from("Unknown error")), - } -} - #[self_referencing] pub struct TransactionContext { card: Card, @@ -140,7 +175,7 @@ impl TransactionContext { parameter1: u8, parameter2: u8, data: Option<&[u8]>, - ) -> Result { + ) -> Result { apdu( self.borrow_transaction(), class, @@ -158,7 +193,7 @@ impl TransactionContext { parameter1: u8, parameter2: u8, data: Option<&[u8]>, - ) -> Result, String> { + ) -> Result, FormattableErrorResponse> { apdu_read_all( self.borrow_transaction(), class,