mirror of
https://github.com/LordGrimmauld/yubi-oath-rs.git
synced 2025-03-04 05:44:40 +01:00
split files
This commit is contained in:
parent
565e924baf
commit
eeafc231be
5 changed files with 430 additions and 374 deletions
116
src/lib_ykoath2/constants.rs
Normal file
116
src/lib_ykoath2/constants.rs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
#[crate_type = "lib"]
|
||||||
|
use openssl::hash::MessageDigest;
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
pub const INS_SELECT: u8 = 0xa4;
|
||||||
|
pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01];
|
||||||
|
|
||||||
|
pub const DEFAULT_PERIOD: u32 = 30;
|
||||||
|
pub const DEFAULT_DIGITS: OathDigits = OathDigits::Six;
|
||||||
|
pub const DEFAULT_IMF: u32 = 0;
|
||||||
|
|
||||||
|
pub enum ErrorResponse {
|
||||||
|
NoSpace = 0x6a84,
|
||||||
|
CommandAborted = 0x6f00,
|
||||||
|
InvalidInstruction = 0x6d00,
|
||||||
|
AuthRequired = 0x6982,
|
||||||
|
WrongSyntax = 0x6a80,
|
||||||
|
GenericError = 0x6581,
|
||||||
|
NoSuchObject = 0x6984,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
pub static ref TOTP_ID_PATTERN: Regex = Regex::new(r"^([A-Za-z0-9]+):([A-Za-z0-9]+):([A-Za-z0-9]+):([0-9]+)?:([0-9]+)$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SuccessResponse {
|
||||||
|
MoreData = 0x61,
|
||||||
|
Okay = 0x9000,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Instruction {
|
||||||
|
Put = 0x01,
|
||||||
|
Delete = 0x02,
|
||||||
|
SetCode = 0x03,
|
||||||
|
Reset = 0x04,
|
||||||
|
Rename = 0x05,
|
||||||
|
List = 0xa1,
|
||||||
|
Calculate = 0xa2,
|
||||||
|
Validate = 0xa3,
|
||||||
|
CalculateAll = 0xa4,
|
||||||
|
SendRemaining = 0xa5,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Mask {
|
||||||
|
Algo = 0x0f,
|
||||||
|
Type = 0xf0,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Tag {
|
||||||
|
Name = 0x71,
|
||||||
|
NameList = 0x72,
|
||||||
|
Key = 0x73,
|
||||||
|
Challenge = 0x74,
|
||||||
|
Response = 0x75,
|
||||||
|
TruncatedResponse = 0x76,
|
||||||
|
Hotp = 0x77,
|
||||||
|
Property = 0x78,
|
||||||
|
Version = 0x79,
|
||||||
|
Imf = 0x7a,
|
||||||
|
Algorithm = 0x7b,
|
||||||
|
Touch = 0x7c,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum HashAlgo {
|
||||||
|
Sha1 = 0x01,
|
||||||
|
Sha256 = 0x02,
|
||||||
|
Sha512 = 0x03,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HashAlgo {
|
||||||
|
pub fn get_message_digest(&self) -> openssl::hash::MessageDigest {
|
||||||
|
match self {
|
||||||
|
HashAlgo::Sha1 => MessageDigest::sha1(),
|
||||||
|
HashAlgo::Sha256 => MessageDigest::sha256(),
|
||||||
|
HashAlgo::Sha512 => MessageDigest::sha512(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Copy, Clone, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum OathType {
|
||||||
|
Totp = 0x10,
|
||||||
|
Hotp = 0x20,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum OathDigits {
|
||||||
|
Six = 6,
|
||||||
|
Eight = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct OathCodeDisplay {
|
||||||
|
code: u32,
|
||||||
|
digits: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OathCodeDisplay {
|
||||||
|
pub fn new(bytes: &[u8; 5]) -> Self {
|
||||||
|
Self {
|
||||||
|
digits: bytes[0],
|
||||||
|
code: BigEndian::read_u32(&bytes[1..5]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(&self) -> String {
|
||||||
|
format!("{:01$}", self.code, usize::from(self.digits))
|
||||||
|
}
|
||||||
|
}
|
76
src/lib_ykoath2/legacy.rs
Normal file
76
src/lib_ykoath2/legacy.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#[crate_type = "lib"]
|
||||||
|
extern crate byteorder;
|
||||||
|
use crate::lib_ykoath2::*;
|
||||||
|
/// Utilities for interacting with YubiKey OATH/TOTP functionality
|
||||||
|
extern crate pcsc;
|
||||||
|
use base32::Alphabet;
|
||||||
|
use iso7816_tlv::simple::{Tag as TlvTag, Tlv};
|
||||||
|
use openssl::hash::MessageDigest;
|
||||||
|
use sha1::Sha1;
|
||||||
|
|
||||||
|
use ouroboros::self_referencing;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::iter::zip;
|
||||||
|
use std::str::{self};
|
||||||
|
|
||||||
|
use apdu_core::{Command, Response};
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use openssl::pkcs5::pbkdf2_hmac;
|
||||||
|
use pcsc::{Card, Transaction};
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct LegacyOathCredential {
|
||||||
|
pub name: String,
|
||||||
|
pub code: OathCode,
|
||||||
|
// TODO: Support this stuff
|
||||||
|
// pub oath_type: OathType,
|
||||||
|
// pub touch: bool,
|
||||||
|
// pub algo: OathAlgo,
|
||||||
|
// pub hidden: bool,
|
||||||
|
// pub steam: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LegacyOathCredential {
|
||||||
|
pub fn new(name: &str, code: OathCode) -> LegacyOathCredential {
|
||||||
|
LegacyOathCredential {
|
||||||
|
name: name.to_string(),
|
||||||
|
code: code,
|
||||||
|
// oath_type: oath_type,
|
||||||
|
// touch: touch,
|
||||||
|
// algo: algo,
|
||||||
|
// hidden: name.starts_with("_hidden:"),
|
||||||
|
// steam: name.starts_with("Steam:"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legacy_format_code(code: u32, digits: OathDigits) -> String {
|
||||||
|
let mut code_string = code.to_string();
|
||||||
|
|
||||||
|
match digits {
|
||||||
|
OathDigits::Six => {
|
||||||
|
if code_string.len() <= 6 {
|
||||||
|
format!("{:0>6}", code_string)
|
||||||
|
} else {
|
||||||
|
code_string.split_off(code_string.len() - 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OathDigits::Eight => {
|
||||||
|
if code_string.len() <= 8 {
|
||||||
|
format!("{:0>8}", code_string)
|
||||||
|
} else {
|
||||||
|
code_string.split_off(code_string.len() - 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,129 +1,29 @@
|
||||||
extern crate byteorder;
|
extern crate byteorder;
|
||||||
|
mod constants;
|
||||||
|
use constants::*;
|
||||||
|
mod transaction;
|
||||||
|
use transaction::*;
|
||||||
|
mod legacy;
|
||||||
|
pub use legacy::*;
|
||||||
/// Utilities for interacting with YubiKey OATH/TOTP functionality
|
/// Utilities for interacting with YubiKey OATH/TOTP functionality
|
||||||
extern crate pcsc;
|
extern crate pcsc;
|
||||||
use base32::Alphabet;
|
use base32::Alphabet;
|
||||||
use iso7816_tlv::simple::{Tag as TlvTag, Tlv};
|
|
||||||
use openssl::hash::MessageDigest;
|
use openssl::hash::MessageDigest;
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
|
|
||||||
use ouroboros::self_referencing;
|
|
||||||
use regex::Regex;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::iter::zip;
|
use std::iter::zip;
|
||||||
use std::str::{self};
|
use std::str::{self};
|
||||||
|
|
||||||
use apdu_core::{Command, Response};
|
|
||||||
|
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use openssl::pkcs5::pbkdf2_hmac;
|
use openssl::pkcs5::pbkdf2_hmac;
|
||||||
use pcsc::{Card, Transaction};
|
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
use std::ffi::CString;
|
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
pub const INS_SELECT: u8 = 0xa4;
|
|
||||||
pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01];
|
|
||||||
|
|
||||||
pub const DEFAULT_PERIOD: u32 = 30;
|
|
||||||
pub const DEFAULT_DIGITS: OathDigits = OathDigits::Six;
|
|
||||||
pub const DEFAULT_IMF: u32 = 0;
|
|
||||||
|
|
||||||
pub enum ErrorResponse {
|
|
||||||
NoSpace = 0x6a84,
|
|
||||||
CommandAborted = 0x6f00,
|
|
||||||
InvalidInstruction = 0x6d00,
|
|
||||||
AuthRequired = 0x6982,
|
|
||||||
WrongSyntax = 0x6a80,
|
|
||||||
GenericError = 0x6581,
|
|
||||||
NoSuchObject = 0x6984,
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref TOTP_ID_PATTERN: Regex = Regex::new(r"^([A-Za-z0-9]+):([A-Za-z0-9]+):([A-Za-z0-9]+):([0-9]+)?:([0-9]+)$").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum SuccessResponse {
|
|
||||||
MoreData = 0x61,
|
|
||||||
Okay = 0x9000,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Instruction {
|
|
||||||
Put = 0x01,
|
|
||||||
Delete = 0x02,
|
|
||||||
SetCode = 0x03,
|
|
||||||
Reset = 0x04,
|
|
||||||
Rename = 0x05,
|
|
||||||
List = 0xa1,
|
|
||||||
Calculate = 0xa2,
|
|
||||||
Validate = 0xa3,
|
|
||||||
CalculateAll = 0xa4,
|
|
||||||
SendRemaining = 0xa5,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum Mask {
|
|
||||||
Algo = 0x0f,
|
|
||||||
Type = 0xf0,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum Tag {
|
|
||||||
Name = 0x71,
|
|
||||||
NameList = 0x72,
|
|
||||||
Key = 0x73,
|
|
||||||
Challenge = 0x74,
|
|
||||||
Response = 0x75,
|
|
||||||
TruncatedResponse = 0x76,
|
|
||||||
Hotp = 0x77,
|
|
||||||
Property = 0x78,
|
|
||||||
Version = 0x79,
|
|
||||||
Imf = 0x7a,
|
|
||||||
Algorithm = 0x7b,
|
|
||||||
Touch = 0x7c,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum HashAlgo {
|
|
||||||
Sha1 = 0x01,
|
|
||||||
Sha256 = 0x02,
|
|
||||||
Sha512 = 0x03,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HashAlgo {
|
|
||||||
pub fn getMessageDigest(&self) -> openssl::hash::MessageDigest {
|
|
||||||
match self {
|
|
||||||
HashAlgo::Sha1 => MessageDigest::sha1(),
|
|
||||||
HashAlgo::Sha256 => MessageDigest::sha256(),
|
|
||||||
HashAlgo::Sha512 => MessageDigest::sha512(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, Eq)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum OathType {
|
|
||||||
Totp = 0x10,
|
|
||||||
Hotp = 0x20,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum OathDigits {
|
|
||||||
Six = 6,
|
|
||||||
Eight = 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ApduResponse {
|
|
||||||
pub buf: Vec<u8>,
|
|
||||||
pub sw1: u8,
|
|
||||||
pub sw2: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct YubiKey<'a> {
|
pub struct YubiKey<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
}
|
}
|
||||||
|
@ -162,8 +62,7 @@ impl<'a> CredentialData<'a> {
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct OathCode {
|
pub struct OathCode {
|
||||||
pub digits: OathDigits,
|
pub display: OathCodeDisplay,
|
||||||
pub value: u32,
|
|
||||||
pub valid_from: u64,
|
pub valid_from: u64,
|
||||||
pub valid_to: u64,
|
pub valid_to: u64,
|
||||||
}
|
}
|
||||||
|
@ -347,178 +246,13 @@ fn format_code(credential: &OathCredential, timestamp: u64, truncated: &[u8]) ->
|
||||||
OathType::Hotp => (timestamp, 0x7FFFFFFFFFFFFFFF),
|
OathType::Hotp => (timestamp, 0x7FFFFFFFFFFFFFFF),
|
||||||
};
|
};
|
||||||
|
|
||||||
let digits = truncated[0] as usize;
|
|
||||||
|
|
||||||
// Convert the truncated bytes to an integer and mask with 0x7FFFFFFF, then apply mod 10^digits
|
|
||||||
let code_value = BigEndian::read_u32(&truncated[1..]) & 0x7FFFFFFF; // Adjust endianess here
|
|
||||||
let mod_value = 10u32.pow(digits as u32);
|
|
||||||
let code_str = format!("{:0width$}", (code_value % mod_value), width = digits);
|
|
||||||
|
|
||||||
OathCode {
|
OathCode {
|
||||||
digits: if digits == 6 {
|
display: OathCodeDisplay::new(truncated[..].try_into().unwrap()),
|
||||||
OathDigits::Six
|
|
||||||
} else if digits == 8 {
|
|
||||||
OathDigits::Eight
|
|
||||||
} else {
|
|
||||||
panic!()
|
|
||||||
},
|
|
||||||
value: code_value,
|
|
||||||
valid_from,
|
valid_from,
|
||||||
valid_to,
|
valid_to,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends the APDU package to the device
|
|
||||||
pub fn apdu(
|
|
||||||
tx: &pcsc::Transaction,
|
|
||||||
class: u8,
|
|
||||||
instruction: u8,
|
|
||||||
parameter1: u8,
|
|
||||||
parameter2: u8,
|
|
||||||
data: Option<&[u8]>,
|
|
||||||
) -> Result<ApduResponse, String> {
|
|
||||||
let command = if let Some(data) = data {
|
|
||||||
Command::new_with_payload(class, instruction, parameter1, parameter2, data)
|
|
||||||
} else {
|
|
||||||
Command::new(class, instruction, parameter1, parameter2)
|
|
||||||
};
|
|
||||||
|
|
||||||
let tx_buf: Vec<u8> = command.into();
|
|
||||||
|
|
||||||
// Construct an empty buffer to hold the response
|
|
||||||
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)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let resp = Response::from(rx_buf);
|
|
||||||
let error_context = to_error_response(resp.trailer.0, resp.trailer.1);
|
|
||||||
|
|
||||||
if let Some(err) = error_context {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ApduResponse {
|
|
||||||
buf: resp.payload.to_vec(),
|
|
||||||
sw1: resp.trailer.0,
|
|
||||||
sw2: resp.trailer.1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apdu_read_all(
|
|
||||||
tx: &pcsc::Transaction,
|
|
||||||
class: u8,
|
|
||||||
instruction: u8,
|
|
||||||
parameter1: u8,
|
|
||||||
parameter2: u8,
|
|
||||||
data: Option<&[u8]>,
|
|
||||||
) -> Result<Vec<u8>, String> {
|
|
||||||
let mut response_buf = Vec::new();
|
|
||||||
let mut resp = apdu(tx, class, instruction, parameter1, parameter2, data)?;
|
|
||||||
response_buf.extend(resp.buf);
|
|
||||||
while resp.sw1 == (SuccessResponse::MoreData as u8) {
|
|
||||||
resp = apdu(tx, 0, Instruction::SendRemaining as u8, 0, 0, None)?;
|
|
||||||
response_buf.extend(resp.buf);
|
|
||||||
}
|
|
||||||
Ok(response_buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_error_response(sw1: u8, sw2: u8) -> Option<String> {
|
|
||||||
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]
|
|
||||||
struct TransactionContext {
|
|
||||||
card: Card,
|
|
||||||
#[borrows(mut card)]
|
|
||||||
#[covariant]
|
|
||||||
transaction: Transaction<'this>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransactionContext {
|
|
||||||
pub fn from_name(name: &str) -> Self {
|
|
||||||
// FIXME: error handling here
|
|
||||||
|
|
||||||
// Establish a PC/SC context
|
|
||||||
let ctx = pcsc::Context::establish(pcsc::Scope::User).unwrap();
|
|
||||||
|
|
||||||
// Connect to the card
|
|
||||||
let card = ctx
|
|
||||||
.connect(
|
|
||||||
&CString::new(name).unwrap(),
|
|
||||||
pcsc::ShareMode::Shared,
|
|
||||||
pcsc::Protocols::ANY,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
TransactionContextBuilder {
|
|
||||||
card,
|
|
||||||
transaction_builder: |c| c.transaction().unwrap(),
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apdu(
|
|
||||||
&self,
|
|
||||||
class: u8,
|
|
||||||
instruction: u8,
|
|
||||||
parameter1: u8,
|
|
||||||
parameter2: u8,
|
|
||||||
data: Option<&[u8]>,
|
|
||||||
) -> Result<ApduResponse, String> {
|
|
||||||
apdu(
|
|
||||||
self.borrow_transaction(),
|
|
||||||
class,
|
|
||||||
instruction,
|
|
||||||
parameter1,
|
|
||||||
parameter2,
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apdu_read_all(
|
|
||||||
&self,
|
|
||||||
class: u8,
|
|
||||||
instruction: u8,
|
|
||||||
parameter1: u8,
|
|
||||||
parameter2: u8,
|
|
||||||
data: Option<&[u8]>,
|
|
||||||
) -> Result<Vec<u8>, String> {
|
|
||||||
apdu_read_all(
|
|
||||||
self.borrow_transaction(),
|
|
||||||
class,
|
|
||||||
instruction,
|
|
||||||
parameter1,
|
|
||||||
parameter2,
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OathSession<'a> {
|
pub struct OathSession<'a> {
|
||||||
version: &'a [u8],
|
version: &'a [u8],
|
||||||
salt: &'a [u8],
|
salt: &'a [u8],
|
||||||
|
@ -589,21 +323,12 @@ impl<'a> OathSession<'a> {
|
||||||
info_map.get(&(Tag::TruncatedResponse as u8)).unwrap(),
|
info_map.get(&(Tag::TruncatedResponse as u8)).unwrap(),
|
||||||
) {
|
) {
|
||||||
assert!(metadata.len() == 5);
|
assert!(metadata.len() == 5);
|
||||||
|
let display = OathCodeDisplay::new(metadata[..].try_into().unwrap());
|
||||||
let name = str::from_utf8(&name_bytes).unwrap();
|
let name = str::from_utf8(&name_bytes).unwrap();
|
||||||
let k_len = metadata.get(0).unwrap();
|
|
||||||
let k_len_enum = match *k_len {
|
|
||||||
6u8 => OathDigits::Six,
|
|
||||||
8u8 => OathDigits::Eight,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let code = BigEndian::read_u32(&metadata[1..5]);
|
|
||||||
|
|
||||||
key_buffer.push(LegacyOathCredential::new(
|
key_buffer.push(LegacyOathCredential::new(
|
||||||
name,
|
name,
|
||||||
OathCode {
|
OathCode {
|
||||||
digits: k_len_enum,
|
display,
|
||||||
value: code,
|
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
valid_to: 0x7FFFFFFFFFFFFFFF,
|
valid_to: 0x7FFFFFFFFFFFFFFF,
|
||||||
},
|
},
|
||||||
|
@ -613,73 +338,6 @@ impl<'a> OathSession<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub struct LegacyOathCredential {
|
|
||||||
pub name: String,
|
|
||||||
pub code: OathCode,
|
|
||||||
// TODO: Support this stuff
|
|
||||||
// pub oath_type: OathType,
|
|
||||||
// pub touch: bool,
|
|
||||||
// pub algo: OathAlgo,
|
|
||||||
// pub hidden: bool,
|
|
||||||
// pub steam: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LegacyOathCredential {
|
|
||||||
pub fn new(name: &str, code: OathCode) -> LegacyOathCredential {
|
|
||||||
LegacyOathCredential {
|
|
||||||
name: name.to_string(),
|
|
||||||
code: code,
|
|
||||||
// oath_type: oath_type,
|
|
||||||
// touch: touch,
|
|
||||||
// algo: algo,
|
|
||||||
// hidden: name.starts_with("_hidden:"),
|
|
||||||
// steam: name.starts_with("Steam:"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_tlv(tag: Tag, value: &[u8]) -> Vec<u8> {
|
|
||||||
Tlv::new(TlvTag::try_from(tag as u8).unwrap(), value.to_vec())
|
|
||||||
.unwrap()
|
|
||||||
.to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tlv_to_map(data: Vec<u8>) -> HashMap<u8, Vec<u8>> {
|
|
||||||
let mut buf: &[u8] = data.leak();
|
|
||||||
let mut parsed_manual = HashMap::new();
|
|
||||||
while !buf.is_empty() {
|
|
||||||
let (r, remaining) = Tlv::parse(buf);
|
|
||||||
buf = remaining;
|
|
||||||
if let Ok(res) = r {
|
|
||||||
parsed_manual.insert(res.tag().into(), res.value().to_vec());
|
|
||||||
} else {
|
|
||||||
println!("tlv parsing error");
|
|
||||||
break; // Exit if parsing fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parsed_manual;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tlv_to_lists(data: Vec<u8>) -> HashMap<u8, Vec<Vec<u8>>> {
|
|
||||||
let mut buf: &[u8] = data.leak();
|
|
||||||
let mut parsed_manual: HashMap<u8, Vec<Vec<u8>>> = HashMap::new();
|
|
||||||
while !buf.is_empty() {
|
|
||||||
let (r, remaining) = Tlv::parse(buf);
|
|
||||||
buf = remaining;
|
|
||||||
if let Ok(res) = r {
|
|
||||||
parsed_manual
|
|
||||||
.entry(res.tag().into())
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(res.value().to_vec());
|
|
||||||
} else {
|
|
||||||
println!("tlv parsing error");
|
|
||||||
break; // Exit if parsing fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parsed_manual;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn time_challenge(timestamp: Option<SystemTime>) -> Vec<u8> {
|
fn time_challenge(timestamp: Option<SystemTime>) -> Vec<u8> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let ts = match timestamp {
|
let ts = match timestamp {
|
||||||
|
@ -701,24 +359,3 @@ fn time_challenge(timestamp: Option<SystemTime>) -> Vec<u8> {
|
||||||
buf.write_u64::<BigEndian>(ts).unwrap();
|
buf.write_u64::<BigEndian>(ts).unwrap();
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn legacy_format_code(code: u32, digits: OathDigits) -> String {
|
|
||||||
let mut code_string = code.to_string();
|
|
||||||
|
|
||||||
match digits {
|
|
||||||
OathDigits::Six => {
|
|
||||||
if code_string.len() <= 6 {
|
|
||||||
format!("{:0>6}", code_string)
|
|
||||||
} else {
|
|
||||||
code_string.split_off(code_string.len() - 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OathDigits::Eight => {
|
|
||||||
if code_string.len() <= 8 {
|
|
||||||
format!("{:0>8}", code_string)
|
|
||||||
} else {
|
|
||||||
code_string.split_off(code_string.len() - 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
227
src/lib_ykoath2/transaction.rs
Normal file
227
src/lib_ykoath2/transaction.rs
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
#[crate_type = "lib"]
|
||||||
|
extern crate byteorder;
|
||||||
|
use crate::lib_ykoath2::*;
|
||||||
|
/// Utilities for interacting with YubiKey OATH/TOTP functionality
|
||||||
|
extern crate pcsc;
|
||||||
|
use base32::Alphabet;
|
||||||
|
use iso7816_tlv::simple::{Tag as TlvTag, Tlv};
|
||||||
|
use openssl::hash::MessageDigest;
|
||||||
|
use sha1::Sha1;
|
||||||
|
|
||||||
|
use ouroboros::self_referencing;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::iter::zip;
|
||||||
|
use std::str::{self};
|
||||||
|
|
||||||
|
use apdu_core::{Command, Response};
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use openssl::pkcs5::pbkdf2_hmac;
|
||||||
|
use pcsc::{Card, Transaction};
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
pub struct ApduResponse {
|
||||||
|
pub buf: Vec<u8>,
|
||||||
|
pub sw1: u8,
|
||||||
|
pub sw2: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends the APDU package to the device
|
||||||
|
fn apdu(
|
||||||
|
tx: &pcsc::Transaction,
|
||||||
|
class: u8,
|
||||||
|
instruction: u8,
|
||||||
|
parameter1: u8,
|
||||||
|
parameter2: u8,
|
||||||
|
data: Option<&[u8]>,
|
||||||
|
) -> Result<ApduResponse, String> {
|
||||||
|
let command = if let Some(data) = data {
|
||||||
|
Command::new_with_payload(class, instruction, parameter1, parameter2, data)
|
||||||
|
} else {
|
||||||
|
Command::new(class, instruction, parameter1, parameter2)
|
||||||
|
};
|
||||||
|
|
||||||
|
let tx_buf: Vec<u8> = command.into();
|
||||||
|
|
||||||
|
// Construct an empty buffer to hold the response
|
||||||
|
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)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = Response::from(rx_buf);
|
||||||
|
let error_context = to_error_response(resp.trailer.0, resp.trailer.1);
|
||||||
|
|
||||||
|
if let Some(err) = error_context {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ApduResponse {
|
||||||
|
buf: resp.payload.to_vec(),
|
||||||
|
sw1: resp.trailer.0,
|
||||||
|
sw2: resp.trailer.1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apdu_read_all(
|
||||||
|
tx: &pcsc::Transaction,
|
||||||
|
class: u8,
|
||||||
|
instruction: u8,
|
||||||
|
parameter1: u8,
|
||||||
|
parameter2: u8,
|
||||||
|
data: Option<&[u8]>,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let mut response_buf = Vec::new();
|
||||||
|
let mut resp = apdu(tx, class, instruction, parameter1, parameter2, data)?;
|
||||||
|
response_buf.extend(resp.buf);
|
||||||
|
while resp.sw1 == (SuccessResponse::MoreData as u8) {
|
||||||
|
resp = apdu(tx, 0, Instruction::SendRemaining as u8, 0, 0, None)?;
|
||||||
|
response_buf.extend(resp.buf);
|
||||||
|
}
|
||||||
|
Ok(response_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_error_response(sw1: u8, sw2: u8) -> Option<String> {
|
||||||
|
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,
|
||||||
|
#[borrows(mut card)]
|
||||||
|
#[covariant]
|
||||||
|
transaction: Transaction<'this>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionContext {
|
||||||
|
pub fn from_name(name: &str) -> Self {
|
||||||
|
// FIXME: error handling here
|
||||||
|
|
||||||
|
// Establish a PC/SC context
|
||||||
|
let ctx = pcsc::Context::establish(pcsc::Scope::User).unwrap();
|
||||||
|
|
||||||
|
// Connect to the card
|
||||||
|
let card = ctx
|
||||||
|
.connect(
|
||||||
|
&CString::new(name).unwrap(),
|
||||||
|
pcsc::ShareMode::Shared,
|
||||||
|
pcsc::Protocols::ANY,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
TransactionContextBuilder {
|
||||||
|
card,
|
||||||
|
transaction_builder: |c| c.transaction().unwrap(),
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apdu(
|
||||||
|
&self,
|
||||||
|
class: u8,
|
||||||
|
instruction: u8,
|
||||||
|
parameter1: u8,
|
||||||
|
parameter2: u8,
|
||||||
|
data: Option<&[u8]>,
|
||||||
|
) -> Result<ApduResponse, String> {
|
||||||
|
apdu(
|
||||||
|
self.borrow_transaction(),
|
||||||
|
class,
|
||||||
|
instruction,
|
||||||
|
parameter1,
|
||||||
|
parameter2,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apdu_read_all(
|
||||||
|
&self,
|
||||||
|
class: u8,
|
||||||
|
instruction: u8,
|
||||||
|
parameter1: u8,
|
||||||
|
parameter2: u8,
|
||||||
|
data: Option<&[u8]>,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
apdu_read_all(
|
||||||
|
self.borrow_transaction(),
|
||||||
|
class,
|
||||||
|
instruction,
|
||||||
|
parameter1,
|
||||||
|
parameter2,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_tlv(tag: Tag, value: &[u8]) -> Vec<u8> {
|
||||||
|
Tlv::new(TlvTag::try_from(tag as u8).unwrap(), value.to_vec())
|
||||||
|
.unwrap()
|
||||||
|
.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tlv_to_map(data: Vec<u8>) -> HashMap<u8, Vec<u8>> {
|
||||||
|
let mut buf: &[u8] = data.leak();
|
||||||
|
let mut parsed_manual = HashMap::new();
|
||||||
|
while !buf.is_empty() {
|
||||||
|
let (r, remaining) = Tlv::parse(buf);
|
||||||
|
buf = remaining;
|
||||||
|
if let Ok(res) = r {
|
||||||
|
parsed_manual.insert(res.tag().into(), res.value().to_vec());
|
||||||
|
} else {
|
||||||
|
println!("tlv parsing error");
|
||||||
|
break; // Exit if parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed_manual;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tlv_to_lists(data: Vec<u8>) -> HashMap<u8, Vec<Vec<u8>>> {
|
||||||
|
let mut buf: &[u8] = data.leak();
|
||||||
|
let mut parsed_manual: HashMap<u8, Vec<Vec<u8>>> = HashMap::new();
|
||||||
|
while !buf.is_empty() {
|
||||||
|
let (r, remaining) = Tlv::parse(buf);
|
||||||
|
buf = remaining;
|
||||||
|
if let Ok(res) = r {
|
||||||
|
parsed_manual
|
||||||
|
.entry(res.tag().into())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(res.value().to_vec());
|
||||||
|
} else {
|
||||||
|
println!("tlv parsing error");
|
||||||
|
break; // Exit if parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed_manual;
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ fn main() {
|
||||||
|
|
||||||
// Enumerate the OATH codes
|
// Enumerate the OATH codes
|
||||||
for oath in codes {
|
for oath in codes {
|
||||||
let code = lib_ykoath2::legacy_format_code(oath.code.value, oath.code.digits);
|
let code = oath.code.display.display();
|
||||||
let name_clone = oath.name.clone();
|
let name_clone = oath.name.clone();
|
||||||
let mut label_vec: Vec<&str> = name_clone.split(":").collect();
|
let mut label_vec: Vec<&str> = name_clone.split(":").collect();
|
||||||
let mut code_entry_label: String = String::from(label_vec.remove(0));
|
let mut code_entry_label: String = String::from(label_vec.remove(0));
|
||||||
|
|
Loading…
Add table
Reference in a new issue