diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..6a75768 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,3 @@ +imports_granularity = "Crate" +format_code_in_doc_comments = true +group_imports = "StdExternalCrate" diff --git a/Cargo.lock b/Cargo.lock index 3e80e4c..34a9edc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,19 +121,7 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "oath-rs-experiments" +name = "lib_ykoath2" version = "0.1.0" dependencies = [ "apdu-core", @@ -150,6 +138,18 @@ dependencies = [ "strum_macros", ] +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "ouroboros" version = "0.18.5" diff --git a/Cargo.toml b/Cargo.toml index 5d804f8..5fe1738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,13 @@ strum = { version = "0.27.0", features = [ ] } strum_macros = "0.27.0" [package] -name = "oath-rs-experiments" +name = "lib_ykoath2" version = "0.1.0" edition = "2021" authors = ["Grimmauld "] description = "experiments with smartcards" license-file = "LICENSE" + +[[example]] +name = "example" +path = "./src/example.rs" \ No newline at end of file diff --git a/src/lib_ykoath2/constants.rs b/src/constants.rs similarity index 79% rename from src/lib_ykoath2/constants.rs rename to src/constants.rs index 27d8709..e54d57d 100644 --- a/src/lib_ykoath2/constants.rs +++ b/src/constants.rs @@ -1,8 +1,7 @@ use std::fmt::Display; use iso7816_tlv::simple::Tlv; -use sha1::{Digest, Sha1}; -use sha2::{Sha256, Sha512}; +use sha1::Digest; use strum::IntoEnumIterator; // 0.17.1 use strum_macros::EnumIter; // 0.17.1 pub const INS_SELECT: u8 = 0xa4; @@ -25,19 +24,6 @@ pub enum ErrorResponse { } 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 { @@ -48,6 +34,22 @@ impl ErrorResponse { } } +impl std::fmt::Display for ErrorResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NoSpace => f.write_str("No Space left on device"), + Self::CommandAborted => f.write_str("Command aborted"), + Self::InvalidInstruction => f.write_str("Invalid instruction"), + Self::AuthRequired => f.write_str("Authentication required"), + Self::WrongSyntax => f.write_str("Wrong syntax"), + Self::GenericError => f.write_str("Generic Error"), + Self::NoSuchObject => f.write_str("No such Object"), + } + } +} + +impl std::error::Error for ErrorResponse {} + #[derive(Debug, EnumIter, Clone, Copy)] #[repr(u16)] pub enum SuccessResponse { @@ -114,18 +116,18 @@ impl HashAlgo { // returns a function capable of hashing a byte array pub fn get_hash_fun(&self) -> impl Fn(&[u8]) -> Vec { match self { - HashAlgo::Sha1 => |m: &[u8]| { - let mut hasher = Sha1::new(); + Self::Sha1 => |m: &[u8]| { + let mut hasher = sha1::Sha1::new(); hasher.update(m); hasher.finalize().to_vec() }, - HashAlgo::Sha256 => |m: &[u8]| { - let mut hasher = Sha256::new(); + Self::Sha256 => |m: &[u8]| { + let mut hasher = sha2::Sha256::new(); hasher.update(m); hasher.finalize().to_vec() }, - HashAlgo::Sha512 => |m: &[u8]| { - let mut hasher = Sha512::new(); + Self::Sha512 => |m: &[u8]| { + let mut hasher = sha2::Sha512::new(); hasher.update(m); hasher.finalize().to_vec() }, @@ -135,9 +137,9 @@ impl HashAlgo { // returns digest output size in number of bytes pub fn digest_size(&self) -> usize { match self { - HashAlgo::Sha1 => 20, - HashAlgo::Sha256 => 32, - HashAlgo::Sha512 => 64, + Self::Sha1 => 20, + Self::Sha256 => 32, + Self::Sha512 => 64, } } } diff --git a/src/main.rs b/src/example.rs similarity index 87% rename from src/main.rs rename to src/example.rs index c6f19b9..1775ceb 100644 --- a/src/main.rs +++ b/src/example.rs @@ -1,12 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause -mod lib_ykoath2; - -use core::time; use lib_ykoath2::OathSession; -use pcsc; -use std::process; -use std::thread; // use crate::args::Cli; // use clap::Parser; @@ -29,14 +23,14 @@ fn main() { // Show message if no YubiKey(s) if yubikeys.len() == 0 { println!("No yubikeys detected"); - process::exit(0); + std::process::exit(0); } // Print device info for all the YubiKeys we detected for yubikey in yubikeys { let device_label: &str = yubikey; println!("Found device with label {}", device_label); - let session = OathSession::new(yubikey); + let session = OathSession::new(yubikey).unwrap(); println!("YubiKey version is {:?}", session.get_version()); for c in session.list_oath_codes().unwrap() { println!("{}", c); @@ -55,7 +49,7 @@ fn main() { println!("No credentials on device {}", device_label); } - thread::sleep(time::Duration::from_secs(5)); // show refresh is working + std::thread::sleep(std::time::Duration::from_secs(5)); // show refresh is working // Enumerate the OATH codes for oath in codes { diff --git a/src/lib_ykoath2/mod.rs b/src/lib.rs similarity index 84% rename from src/lib_ykoath2/mod.rs rename to src/lib.rs index 76d29a2..f29afe7 100644 --- a/src/lib_ykoath2/mod.rs +++ b/src/lib.rs @@ -4,23 +4,13 @@ mod transaction; use transaction::*; mod oath_credential; mod oath_credentialid; -use oath_credential::*; -use oath_credentialid::*; /// Utilities for interacting with YubiKey OATH/TOTP functionality -extern crate pcsc; -use pbkdf2::pbkdf2_hmac_array; -use sha1::Sha1; - -use std::{ - fmt::Display, - str::{self}, - time::Duration, -}; +use std::{fmt::Display, time::Duration, time::SystemTime}; use base64::{engine::general_purpose, Engine as _}; use hmac::{Hmac, Mac}; - -use std::time::SystemTime; +use oath_credential::*; +use oath_credentialid::*; fn _get_device_id(salt: Vec) -> String { let result = HashAlgo::Sha256.get_hash_fun()(salt.leak()); @@ -32,13 +22,13 @@ fn _get_device_id(salt: Vec) -> String { return general_purpose::URL_SAFE_NO_PAD.encode(hash_16_bytes); } fn _hmac_sha1(key: &[u8], message: &[u8]) -> Vec { - let mut mac = Hmac::::new_from_slice(key).expect("Invalid key length"); + let mut mac = Hmac::::new_from_slice(key).expect("Invalid key length"); mac.update(message); mac.finalize().into_bytes().to_vec() } fn _derive_key(salt: &[u8], passphrase: &str) -> Vec { - pbkdf2_hmac_array::(passphrase.as_bytes(), salt, 1000).to_vec() + pbkdf2::pbkdf2_hmac_array::(passphrase.as_bytes(), salt, 1000).to_vec() } fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec { @@ -143,11 +133,10 @@ impl<'a> RefreshableOathCredential<'a> { } impl<'a> OathSession<'a> { - pub fn new(name: &str) -> Self { - let transaction_context = TransactionContext::from_name(name); - let info_buffer = transaction_context - .apdu_read_all(0, INS_SELECT, 0x04, 0, Some(&OATH_AID)) - .unwrap(); + pub fn new(name: &str) -> Result { + let transaction_context = TransactionContext::from_name(name)?; + let info_buffer = + transaction_context.apdu_read_all(0, INS_SELECT, 0x04, 0, Some(&OATH_AID))?; let info_map = tlv_to_map(info_buffer); for (tag, data) in &info_map { @@ -155,7 +144,7 @@ impl<'a> OathSession<'a> { println!("{:?}: {:?}", tag, data); } - OathSession { + Ok(Self { version: clone_with_lifetime( info_map.get(&(Tag::Version as u8)).unwrap_or(&vec![0u8; 0]), ) @@ -170,7 +159,7 @@ impl<'a> OathSession<'a> { .leak(), name: name.to_string(), transaction_context, - } + }) } pub fn get_version(&self) -> &[u8] { @@ -181,7 +170,7 @@ impl<'a> OathSession<'a> { &self, old: CredentialIDData, new: CredentialIDData, - ) -> Result { + ) -> Result { // require_version(self.version, (5, 3, 1)) TODO: version checking self.transaction_context.apdu( 0, @@ -193,10 +182,7 @@ impl<'a> OathSession<'a> { Ok(new) } - pub fn delete_code( - &self, - cred: OathCredential, - ) -> Result { + pub fn delete_code(&self, cred: OathCredential) -> Result { self.transaction_context.apdu( 0, Instruction::Delete as u8, @@ -210,9 +196,9 @@ impl<'a> OathSession<'a> { &self, cred: OathCredential, timestamp_sys: Option, - ) -> Result { + ) -> Result { if self.name != cred.device_id { - return Err(FormattableErrorResponse::DeviceMismatchError); + return Err(Error::DeviceMismatchError); } let timestamp = time_to_u64(timestamp_sys.unwrap_or_else(SystemTime::now)); @@ -233,22 +219,18 @@ impl<'a> OathSession<'a> { Some(&data), ); - let meta = - TlvIter::from_vec(resp?) - .next() - .ok_or(FormattableErrorResponse::ParsingError( - "No credentials to unpack found in response".to_string(), - ))?; + let meta = TlvIter::from_vec(resp?).next().ok_or(Error::ParsingError( + "No credentials to unpack found in response".to_string(), + ))?; - OathCodeDisplay::from_tlv(meta).ok_or(FormattableErrorResponse::ParsingError( + OathCodeDisplay::from_tlv(meta).ok_or(Error::ParsingError( "error parsing calculation response".to_string(), )) } - /// Read the OATH codes from the device, calculate TOTP codes that don't need touch - pub fn calculate_oath_codes( - &self, - ) -> Result, FormattableErrorResponse> { + /// Read the OATH codes from the device, calculate TOTP codes that don't + /// need touch + pub fn calculate_oath_codes(&self) -> Result, Error> { let timestamp = SystemTime::now(); // Request OATH codes from device let response = self.transaction_context.apdu_read_all( @@ -266,8 +248,8 @@ impl<'a> OathSession<'a> { let id_data = CredentialIDData::from_tlv(cred_id.value(), meta.tag()); let code = OathCodeDisplay::from_tlv(meta); - /* println!("id bytes: {:?}", cred_id.value()); - println!("id recon: {:?}", id_data.format_cred_id()); */ + // println!("id bytes: {:?}", cred_id.value()); + // println!("id recon: {:?}", id_data.format_cred_id()); let cred = OathCredential { device_id: self.name.clone(), @@ -283,7 +265,7 @@ impl<'a> OathSession<'a> { return Ok(key_buffer); } - pub fn list_oath_codes(&self) -> Result, FormattableErrorResponse> { + pub fn list_oath_codes(&self) -> Result, Error> { // Request OATH codes from device let response = self.transaction_context diff --git a/src/lib_ykoath2/oath_credential.rs b/src/oath_credential.rs similarity index 74% rename from src/lib_ykoath2/oath_credential.rs rename to src/oath_credential.rs index 70afe8f..1712ef6 100644 --- a/src/lib_ykoath2/oath_credential.rs +++ b/src/oath_credential.rs @@ -1,20 +1,9 @@ -#[crate_type = "lib"] -use crate::lib_ykoath2::*; -/// Utilities for interacting with YubiKey OATH/TOTP functionality -extern crate pcsc; -use pbkdf2::pbkdf2_hmac_array; -use regex::Regex; -use sha1::Sha1; +use std::{ + cmp::Ordering, + hash::{Hash, Hasher}, +}; -use std::str::{self}; - -use base64::{engine::general_purpose, Engine as _}; -use hmac::{Hmac, Mac}; - -use std::cmp::Ordering; -use std::hash::{Hash, Hasher}; - -use std::time::SystemTime; +use crate::CredentialIDData; #[derive(Debug, Clone)] pub struct OathCredential { diff --git a/src/lib_ykoath2/oath_credentialid.rs b/src/oath_credentialid.rs similarity index 91% rename from src/lib_ykoath2/oath_credentialid.rs rename to src/oath_credentialid.rs index f005902..f7fe075 100644 --- a/src/lib_ykoath2/oath_credentialid.rs +++ b/src/oath_credentialid.rs @@ -1,13 +1,8 @@ -#![crate_type = "lib"] -use crate::lib_ykoath2::*; -/// Utilities for interacting with YubiKey OATH/TOTP functionality -extern crate pcsc; +use std::fmt::Display; + use regex::Regex; -use std::{ - fmt::Write, - str::{self}, -}; +use crate::{to_tlv, OathType, Tag, DEFAULT_PERIOD}; #[derive(Debug, Eq, PartialEq, Clone)] pub struct CredentialIDData { @@ -52,7 +47,7 @@ impl CredentialIDData { // Function to parse the credential ID fn parse_cred_id(cred_id: &[u8], oath_type: OathType) -> (Option, String, u32) { - let data = match str::from_utf8(cred_id) { + let data = match std::str::from_utf8(cred_id) { Ok(d) => d, Err(_) => return (None, String::new(), 0), // Handle invalid UTF-8 }; diff --git a/src/lib_ykoath2/transaction.rs b/src/transaction.rs similarity index 66% rename from src/lib_ykoath2/transaction.rs rename to src/transaction.rs index a2bdf45..0b7484f 100644 --- a/src/lib_ykoath2/transaction.rs +++ b/src/transaction.rs @@ -1,22 +1,14 @@ -#[crate_type = "lib"] -use crate::lib_ykoath2::*; -/// Utilities for interacting with YubiKey OATH/TOTP functionality -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 std::{collections::HashMap, ffi::CString, fmt::Display}; use apdu_core::{Command, Response}; - +use iso7816_tlv::simple::{Tag as TlvTag, Tlv}; +use ouroboros::self_referencing; use pcsc::{Card, Transaction}; -use std::ffi::CString; +use crate::{ErrorResponse, Instruction, SuccessResponse, Tag}; #[derive(PartialEq, Eq, Debug)] -pub enum FormattableErrorResponse { - NoError, +pub enum Error { Unknown(String), Protocol(ErrorResponse), PcscError(pcsc::Error), @@ -24,50 +16,37 @@ pub enum FormattableErrorResponse { DeviceMismatchError, } -impl FormattableErrorResponse { - pub fn from_apdu_response(sw1: u8, sw2: u8) -> FormattableErrorResponse { +impl Error { + fn from_apdu_response(sw1: u8, sw2: u8) -> Result<(), Self> { let code: u16 = (sw1 as u16 | sw2 as u16) << 8; if let Some(e) = ErrorResponse::any_match(code) { - return FormattableErrorResponse::Protocol(e); + return Err(Self::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(), + return Ok(()); } + Err(Self::Unknown(String::from("Unknown error"))) } } -impl Display for FormattableErrorResponse { +impl From for Error { + fn from(value: pcsc::Error) -> Self { + Self::PcscError(value) + } +} + +impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.as_string()) + match self { + Self::Unknown(msg) => f.write_str(msg), + Self::Protocol(error_response) => f.write_fmt(format_args!("{}", error_response)), + Self::PcscError(error) => f.write_fmt(format_args!("{}", error)), + Self::ParsingError(msg) => f.write_str(msg), + Self::DeviceMismatchError => f.write_str("Devices do not match"), + } } } @@ -85,7 +64,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 { @@ -98,19 +77,9 @@ fn apdu( let mut rx_buf = [0; pcsc::MAX_BUFFER_SIZE]; // 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(FormattableErrorResponse::from_transmit(err)), - }; - + let rx_buf = tx.transmit(&tx_buf, &mut rx_buf)?; let resp = Response::from(rx_buf); - let error_context = - FormattableErrorResponse::from_apdu_response(resp.trailer.0, resp.trailer.1); - - if !error_context.is_ok() { - return Err(error_context); - } + Error::from_apdu_response(resp.trailer.0, resp.trailer.1)?; Ok(ApduResponse { buf: resp.payload.to_vec(), @@ -126,7 +95,7 @@ fn apdu_read_all( parameter1: u8, parameter2: u8, data: Option<&[u8]>, -) -> Result, FormattableErrorResponse> { +) -> Result, Error> { let mut response_buf = Vec::new(); let mut resp = apdu(tx, class, instruction, parameter1, parameter2, data)?; response_buf.extend(resp.buf); @@ -146,26 +115,22 @@ pub struct TransactionContext { } impl TransactionContext { - pub fn from_name(name: &str) -> Self { - // FIXME: error handling here - + pub fn from_name(name: &str) -> Result { // Establish a PC/SC context - let ctx = pcsc::Context::establish(pcsc::Scope::User).unwrap(); + let ctx = pcsc::Context::establish(pcsc::Scope::User)?; // Connect to the card - let card = ctx - .connect( - &CString::new(name).unwrap(), - pcsc::ShareMode::Shared, - pcsc::Protocols::ANY, - ) - .unwrap(); + let card = ctx.connect( + &CString::new(name).unwrap(), + pcsc::ShareMode::Shared, + pcsc::Protocols::ANY, + )?; - TransactionContextBuilder { + Ok(TransactionContextBuilder { card, transaction_builder: |c| c.transaction().unwrap(), } - .build() + .build()) } pub fn apdu( @@ -175,7 +140,7 @@ impl TransactionContext { parameter1: u8, parameter2: u8, data: Option<&[u8]>, - ) -> Result { + ) -> Result { apdu( self.borrow_transaction(), class, @@ -193,7 +158,7 @@ impl TransactionContext { parameter1: u8, parameter2: u8, data: Option<&[u8]>, - ) -> Result, FormattableErrorResponse> { + ) -> Result, Error> { apdu_read_all( self.borrow_transaction(), class,