mirror of
https://github.com/LordGrimmauld/yubi-oath-rs.git
synced 2025-03-04 05:44:40 +01:00
doc: document constants
This commit is contained in:
parent
c596cfbcd9
commit
b7b4e79c14
5 changed files with 59 additions and 70 deletions
|
@ -2,14 +2,24 @@ use std::{fmt::Display, time::Duration};
|
||||||
|
|
||||||
use iso7816_tlv::simple::Tlv;
|
use iso7816_tlv::simple::Tlv;
|
||||||
use sha1::Digest;
|
use sha1::Digest;
|
||||||
|
|
||||||
|
/// te apdu instruction required to select the Oath application on the YubiKey
|
||||||
pub const INS_SELECT: u8 = 0xa4;
|
pub const INS_SELECT: u8 = 0xa4;
|
||||||
|
|
||||||
|
/// the stream of data bytes required to select the Oath application on the YubiKey
|
||||||
pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01];
|
pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01];
|
||||||
|
|
||||||
|
/// The default duration that generated codes are valid for is 30 seconds.
|
||||||
|
/// Non-default values are saved and retrieved from the YubiKey.
|
||||||
|
/// If not explicitly declared otherwise, this is the fallback that will be used.
|
||||||
pub const DEFAULT_PERIOD: Duration = Duration::from_secs(30);
|
pub const DEFAULT_PERIOD: Duration = Duration::from_secs(30);
|
||||||
pub const DEFAULT_DIGITS: OathDigits = OathDigits::Six;
|
|
||||||
pub const DEFAULT_IMF: u32 = 0;
|
/// Oath keys on the YubiKey are stored by a handle.
|
||||||
|
/// If the original handle is too long, it will be shortend to an hmac.
|
||||||
|
/// If the key is too short, it will be padded to have the minimum length.
|
||||||
pub const HMAC_MINIMUM_KEY_SIZE: usize = 14;
|
pub const HMAC_MINIMUM_KEY_SIZE: usize = 14;
|
||||||
|
|
||||||
|
/// Error Response codes as returned by the YubiKey
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
pub enum ErrorResponse {
|
pub enum ErrorResponse {
|
||||||
|
@ -60,6 +70,7 @@ impl std::fmt::Display for ErrorResponse {
|
||||||
|
|
||||||
impl std::error::Error for ErrorResponse {}
|
impl std::error::Error for ErrorResponse {}
|
||||||
|
|
||||||
|
/// Success response codes as returned by the YubiKey
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
pub enum SuccessResponse {
|
pub enum SuccessResponse {
|
||||||
|
@ -79,6 +90,7 @@ impl SuccessResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set of possible instructions to use with the YubiKey via apdu
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum Instruction {
|
pub enum Instruction {
|
||||||
Put = 0x01,
|
Put = 0x01,
|
||||||
|
@ -93,12 +105,7 @@ pub enum Instruction {
|
||||||
SendRemaining = 0xa5,
|
SendRemaining = 0xa5,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
/// possible tlv tag values to use with apdu
|
||||||
pub enum Mask {
|
|
||||||
Algo = 0x0f,
|
|
||||||
Type = 0xf0,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum Tag {
|
pub enum Tag {
|
||||||
Name = 0x71,
|
Name = 0x71,
|
||||||
|
@ -115,6 +122,7 @@ pub enum Tag {
|
||||||
Touch = 0x7c,
|
Touch = 0x7c,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Different Hash algorithms supported by the YubiKey to generate OTPs
|
||||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum HashAlgo {
|
pub enum HashAlgo {
|
||||||
|
@ -124,7 +132,8 @@ pub enum HashAlgo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HashAlgo {
|
impl HashAlgo {
|
||||||
// returns a function capable of hashing a byte array
|
/// returns a function capable of hashing a byte array
|
||||||
|
/// necessary to be able to validate keys before enrolling them on the hardware key
|
||||||
pub fn get_hash_fun(&self) -> impl Fn(&[u8]) -> Vec<u8> {
|
pub fn get_hash_fun(&self) -> impl Fn(&[u8]) -> Vec<u8> {
|
||||||
match self {
|
match self {
|
||||||
Self::Sha1 => |m: &[u8]| {
|
Self::Sha1 => |m: &[u8]| {
|
||||||
|
@ -145,7 +154,7 @@ impl HashAlgo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns digest output size in number of bytes
|
/// returns digest output size in number of bytes
|
||||||
pub fn digest_size(&self) -> usize {
|
pub fn digest_size(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::Sha1 => 20,
|
Self::Sha1 => 20,
|
||||||
|
@ -155,6 +164,7 @@ impl HashAlgo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// different types of oath to generate OTPs
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)]
|
#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum OathType {
|
pub enum OathType {
|
||||||
|
@ -162,12 +172,8 @@ pub enum OathType {
|
||||||
Hotp = 0x20,
|
Hotp = 0x20,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
/// describes display information of a code, keeping track of the code and number of digits
|
||||||
pub enum OathDigits {
|
/// digits *should* be either 6 or 8, but nothing is preventing the yubikey from returning e.g. 7 digits, which would "just work".
|
||||||
Six = 6,
|
|
||||||
Eight = 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Hash, Eq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Hash, Eq, Copy, Clone)]
|
||||||
pub struct OathCodeDisplay {
|
pub struct OathCodeDisplay {
|
||||||
code: u32,
|
code: u32,
|
||||||
|
@ -181,19 +187,24 @@ impl Display for OathCodeDisplay {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OathCodeDisplay {
|
impl OathCodeDisplay {
|
||||||
|
/// extracts display information (digits and code) from a `Tlv` instance
|
||||||
pub fn from_tlv(tlv: Tlv) -> Option<Self> {
|
pub fn from_tlv(tlv: Tlv) -> Option<Self> {
|
||||||
if Into::<u8>::into(tlv.tag()) == (Tag::TruncatedResponse as u8) && tlv.value().len() == 5 {
|
if Into::<u8>::into(tlv.tag()) == (Tag::TruncatedResponse as u8) {
|
||||||
let display = OathCodeDisplay::new(tlv.value()[..].try_into().unwrap());
|
TryInto::<&[u8; 5]>::try_into(tlv.value())
|
||||||
Some(display)
|
.ok()
|
||||||
|
.and_then(OathCodeDisplay::new)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(bytes: &[u8; 5]) -> Self {
|
/// extract display information from a 5-bytes buffer
|
||||||
Self {
|
pub fn new(bytes: &[u8; 5]) -> Option<Self> {
|
||||||
|
TryInto::<[u8; 4]>::try_into(&bytes[1..])
|
||||||
|
.ok()
|
||||||
|
.map(|code_bytes| Self {
|
||||||
digits: bytes[0],
|
digits: bytes[0],
|
||||||
code: u32::from_be_bytes((&bytes[1..5]).try_into().unwrap()),
|
code: u32::from_be_bytes(code_bytes),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
use ykoath2::constants::{HashAlgo, OathDigits, OathType, DEFAULT_PERIOD};
|
use ykoath2::constants::{HashAlgo, OathType, DEFAULT_PERIOD};
|
||||||
use ykoath2::oath_credential::OathCredential;
|
use ykoath2::oath_credential::OathCredential;
|
||||||
use ykoath2::oath_credentialid::CredentialIDData;
|
use ykoath2::oath_credential_id::CredentialIDData;
|
||||||
use ykoath2::OathSession;
|
use ykoath2::OathSession;
|
||||||
// use crate::args::Cli;
|
// use crate::args::Cli;
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ fn main() {
|
||||||
cred.clone(),
|
cred.clone(),
|
||||||
"f5up4ub3dw".as_bytes(),
|
"f5up4ub3dw".as_bytes(),
|
||||||
HashAlgo::Sha256,
|
HashAlgo::Sha256,
|
||||||
OathDigits::Six,
|
6,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
60
src/lib.rs
60
src/lib.rs
|
@ -1,12 +1,24 @@
|
||||||
|
//! # Rust bindings to the Oath application on the YubiKey
|
||||||
|
//!
|
||||||
|
//! Bindings closely resemble the reverse-engineered [python library](https://github.com/Yubico/yubikey-manager/blob/main/yubikit/oath.py),
|
||||||
|
//! as well as the discontinued crate [ykoath](https://crates.io/crates/ykoath)
|
||||||
|
//!
|
||||||
|
|
||||||
|
/// constants relevant for apdu, pcsc, error handling
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
|
|
||||||
|
/// OathCredential stores one credential
|
||||||
pub mod oath_credential;
|
pub mod oath_credential;
|
||||||
pub mod oath_credentialid;
|
|
||||||
|
/// OathCredentialId stores information about issuer, credential name, time raster, display
|
||||||
|
pub mod oath_credential_id;
|
||||||
|
|
||||||
mod refreshable_oath_credential;
|
mod refreshable_oath_credential;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
|
||||||
use constants::*;
|
use constants::*;
|
||||||
use oath_credential::*;
|
use oath_credential::*;
|
||||||
use oath_credentialid::*;
|
use oath_credential_id::*;
|
||||||
use refreshable_oath_credential::*;
|
use refreshable_oath_credential::*;
|
||||||
use transaction::*;
|
use transaction::*;
|
||||||
|
|
||||||
|
@ -38,6 +50,7 @@ fn time_challenge(timestamp: Option<SystemTime>, period: Option<Duration>) -> [u
|
||||||
.to_be_bytes()
|
.to_be_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// keeps track of transactions with a named YubiKey
|
||||||
pub struct OathSession {
|
pub struct OathSession {
|
||||||
version: Vec<u8>,
|
version: Vec<u8>,
|
||||||
salt: Vec<u8>,
|
salt: Vec<u8>,
|
||||||
|
@ -97,10 +110,6 @@ impl OathSession {
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !self.locked {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let hmac = hmac_sha1(key, &chal);
|
let hmac = hmac_sha1(key, &chal);
|
||||||
let random_chal = getrandom::u64()?.to_be_bytes();
|
let random_chal = getrandom::u64()?.to_be_bytes();
|
||||||
let data = &[
|
let data = &[
|
||||||
|
@ -120,14 +129,13 @@ impl OathSession {
|
||||||
self.locked = false;
|
self.locked = false;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Authentication)
|
Err(Error::Unknown(
|
||||||
|
"Unlocking session failed unexpectedly".to_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_key(&mut self, key: &[u8]) -> Result<(), Error> {
|
pub fn set_key(&mut self, key: &[u8]) -> Result<(), Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
let random_chal = getrandom::u64()?.to_be_bytes();
|
let random_chal = getrandom::u64()?.to_be_bytes();
|
||||||
let hmac = hmac_sha1(key, &random_chal);
|
let hmac = hmac_sha1(key, &random_chal);
|
||||||
let data = &[
|
let data = &[
|
||||||
|
@ -152,10 +160,6 @@ impl OathSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unset_key(&mut self) -> Result<(), Error> {
|
pub fn unset_key(&mut self) -> Result<(), Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.transaction_context.apdu(
|
self.transaction_context.apdu(
|
||||||
0,
|
0,
|
||||||
Instruction::SetCode as u8,
|
Instruction::SetCode as u8,
|
||||||
|
@ -173,10 +177,6 @@ impl OathSession {
|
||||||
old: CredentialIDData,
|
old: CredentialIDData,
|
||||||
new: CredentialIDData,
|
new: CredentialIDData,
|
||||||
) -> Result<CredentialIDData, Error> {
|
) -> Result<CredentialIDData, Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.require_version(vec![5, 3, 1])?;
|
self.require_version(vec![5, 3, 1])?;
|
||||||
self.transaction_context.apdu(
|
self.transaction_context.apdu(
|
||||||
0,
|
0,
|
||||||
|
@ -189,10 +189,6 @@ impl OathSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_code(&self, cred: OathCredential) -> Result<(), Error> {
|
pub fn delete_code(&self, cred: OathCredential) -> Result<(), Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.transaction_context.apdu(
|
self.transaction_context.apdu(
|
||||||
0,
|
0,
|
||||||
Instruction::Delete as u8,
|
Instruction::Delete as u8,
|
||||||
|
@ -208,13 +204,9 @@ impl OathSession {
|
||||||
cred: OathCredential,
|
cred: OathCredential,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
algo: HashAlgo,
|
algo: HashAlgo,
|
||||||
digits: OathDigits,
|
digits: u8,
|
||||||
counter: Option<u32>,
|
counter: Option<u32>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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
|
let len_to_copy = secret_short.len().min(HMAC_MINIMUM_KEY_SIZE); // Avoid copying more than 14
|
||||||
|
@ -226,7 +218,7 @@ impl OathSession {
|
||||||
to_tlv(
|
to_tlv(
|
||||||
Tag::Key,
|
Tag::Key,
|
||||||
&[
|
&[
|
||||||
[(cred.id_data.oath_type as u8) | (algo as u8), digits as u8].to_vec(),
|
[(cred.id_data.oath_type as u8) | (algo as u8), digits].to_vec(),
|
||||||
secret_padded.to_vec(),
|
secret_padded.to_vec(),
|
||||||
]
|
]
|
||||||
.concat(),
|
.concat(),
|
||||||
|
@ -270,10 +262,6 @@ impl OathSession {
|
||||||
cred: &OathCredential,
|
cred: &OathCredential,
|
||||||
timestamp_sys: Option<SystemTime>,
|
timestamp_sys: Option<SystemTime>,
|
||||||
) -> Result<OathCodeDisplay, Error> {
|
) -> Result<OathCodeDisplay, Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.name != cred.device_id {
|
if self.name != cred.device_id {
|
||||||
return Err(Error::DeviceMismatch);
|
return Err(Error::DeviceMismatch);
|
||||||
}
|
}
|
||||||
|
@ -308,10 +296,6 @@ impl OathSession {
|
||||||
/// Read the OATH codes from the device, calculate TOTP codes that don't
|
/// Read the OATH codes from the device, calculate TOTP codes that don't
|
||||||
/// need touch
|
/// need touch
|
||||||
pub fn calculate_oath_codes(&self) -> Result<Vec<RefreshableOathCredential>, Error> {
|
pub fn calculate_oath_codes(&self) -> Result<Vec<RefreshableOathCredential>, Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
let timestamp = SystemTime::now();
|
let timestamp = SystemTime::now();
|
||||||
// Request OATH codes from device
|
// Request OATH codes from device
|
||||||
let response = self.transaction_context.apdu_read_all(
|
let response = self.transaction_context.apdu_read_all(
|
||||||
|
@ -347,10 +331,6 @@ impl OathSession {
|
||||||
Ok(key_buffer)
|
Ok(key_buffer)
|
||||||
}
|
}
|
||||||
pub fn list_oath_codes(&self) -> Result<Vec<CredentialIDData>, Error> {
|
pub fn list_oath_codes(&self) -> Result<Vec<CredentialIDData>, Error> {
|
||||||
if self.locked {
|
|
||||||
return Err(Error::Authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request OATH codes from device
|
// Request OATH codes from device
|
||||||
let response =
|
let response =
|
||||||
self.transaction_context
|
self.transaction_context
|
||||||
|
|
|
@ -14,14 +14,13 @@ pub enum Error {
|
||||||
Pcsc(pcsc::Error),
|
Pcsc(pcsc::Error),
|
||||||
Parsing(String),
|
Parsing(String),
|
||||||
DeviceMismatch,
|
DeviceMismatch,
|
||||||
Authentication,
|
|
||||||
Random(getrandom::Error),
|
Random(getrandom::Error),
|
||||||
Version(Vec<u8>, Vec<u8>),
|
Version(Vec<u8>, Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
fn from_apdu_response(sw1: u8, sw2: u8) -> Result<(), Self> {
|
fn from_apdu_response(sw1: u8, sw2: u8) -> Result<(), Self> {
|
||||||
let code: u16 = (sw1 as u16 | sw2 as u16) << 8;
|
let code: u16 = ((sw1 as u16) << 8) | (sw2 as u16);
|
||||||
if let Some(e) = ErrorResponse::any_match(code) {
|
if let Some(e) = ErrorResponse::any_match(code) {
|
||||||
return Err(Self::Protocol(e));
|
return Err(Self::Protocol(e));
|
||||||
}
|
}
|
||||||
|
@ -55,7 +54,6 @@ impl Display for Error {
|
||||||
Self::Pcsc(error) => f.write_fmt(format_args!("{}", error)),
|
Self::Pcsc(error) => f.write_fmt(format_args!("{}", error)),
|
||||||
Self::Parsing(msg) => f.write_str(msg),
|
Self::Parsing(msg) => f.write_str(msg),
|
||||||
Self::DeviceMismatch => f.write_str("Devices do not match"),
|
Self::DeviceMismatch => f.write_str("Devices do not match"),
|
||||||
Self::Authentication => f.write_str("Authentication failure"),
|
|
||||||
Self::Random(error_response) => f.write_fmt(format_args!("{}", error_response)),
|
Self::Random(error_response) => f.write_fmt(format_args!("{}", error_response)),
|
||||||
Self::Version(ver, req) => f.write_fmt(format_args!(
|
Self::Version(ver, req) => f.write_fmt(format_args!(
|
||||||
"Version requirement not met: is {}, required {}",
|
"Version requirement not met: is {}, required {}",
|
||||||
|
|
Loading…
Add table
Reference in a new issue