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 sha1::Digest;
|
||||
|
||||
/// te apdu instruction required to select the Oath application on the YubiKey
|
||||
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];
|
||||
|
||||
/// 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_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;
|
||||
|
||||
/// Error Response codes as returned by the YubiKey
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum ErrorResponse {
|
||||
|
@ -60,6 +70,7 @@ impl std::fmt::Display for ErrorResponse {
|
|||
|
||||
impl std::error::Error for ErrorResponse {}
|
||||
|
||||
/// Success response codes as returned by the YubiKey
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u16)]
|
||||
pub enum SuccessResponse {
|
||||
|
@ -79,6 +90,7 @@ impl SuccessResponse {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set of possible instructions to use with the YubiKey via apdu
|
||||
#[repr(u8)]
|
||||
pub enum Instruction {
|
||||
Put = 0x01,
|
||||
|
@ -93,12 +105,7 @@ pub enum Instruction {
|
|||
SendRemaining = 0xa5,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum Mask {
|
||||
Algo = 0x0f,
|
||||
Type = 0xf0,
|
||||
}
|
||||
|
||||
/// possible tlv tag values to use with apdu
|
||||
#[repr(u8)]
|
||||
pub enum Tag {
|
||||
Name = 0x71,
|
||||
|
@ -115,6 +122,7 @@ pub enum Tag {
|
|||
Touch = 0x7c,
|
||||
}
|
||||
|
||||
/// Different Hash algorithms supported by the YubiKey to generate OTPs
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum HashAlgo {
|
||||
|
@ -124,7 +132,8 @@ pub enum 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> {
|
||||
match self {
|
||||
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 {
|
||||
match self {
|
||||
Self::Sha1 => 20,
|
||||
|
@ -155,6 +164,7 @@ impl HashAlgo {
|
|||
}
|
||||
}
|
||||
|
||||
/// different types of oath to generate OTPs
|
||||
#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum OathType {
|
||||
|
@ -162,12 +172,8 @@ pub enum OathType {
|
|||
Hotp = 0x20,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum OathDigits {
|
||||
Six = 6,
|
||||
Eight = 8,
|
||||
}
|
||||
|
||||
/// describes display information of a code, keeping track of the code and number of digits
|
||||
/// digits *should* be either 6 or 8, but nothing is preventing the yubikey from returning e.g. 7 digits, which would "just work".
|
||||
#[derive(Debug, PartialEq, Hash, Eq, Copy, Clone)]
|
||||
pub struct OathCodeDisplay {
|
||||
code: u32,
|
||||
|
@ -181,19 +187,24 @@ impl Display for OathCodeDisplay {
|
|||
}
|
||||
|
||||
impl OathCodeDisplay {
|
||||
/// extracts display information (digits and code) from a `Tlv` instance
|
||||
pub fn from_tlv(tlv: Tlv) -> Option<Self> {
|
||||
if Into::<u8>::into(tlv.tag()) == (Tag::TruncatedResponse as u8) && tlv.value().len() == 5 {
|
||||
let display = OathCodeDisplay::new(tlv.value()[..].try_into().unwrap());
|
||||
Some(display)
|
||||
if Into::<u8>::into(tlv.tag()) == (Tag::TruncatedResponse as u8) {
|
||||
TryInto::<&[u8; 5]>::try_into(tlv.value())
|
||||
.ok()
|
||||
.and_then(OathCodeDisplay::new)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(bytes: &[u8; 5]) -> Self {
|
||||
Self {
|
||||
digits: bytes[0],
|
||||
code: u32::from_be_bytes((&bytes[1..5]).try_into().unwrap()),
|
||||
}
|
||||
/// extract display information from a 5-bytes buffer
|
||||
pub fn new(bytes: &[u8; 5]) -> Option<Self> {
|
||||
TryInto::<[u8; 4]>::try_into(&bytes[1..])
|
||||
.ok()
|
||||
.map(|code_bytes| Self {
|
||||
digits: bytes[0],
|
||||
code: u32::from_be_bytes(code_bytes),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// 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_credentialid::CredentialIDData;
|
||||
use ykoath2::oath_credential_id::CredentialIDData;
|
||||
use ykoath2::OathSession;
|
||||
// use crate::args::Cli;
|
||||
|
||||
|
@ -55,7 +55,7 @@ fn main() {
|
|||
cred.clone(),
|
||||
"f5up4ub3dw".as_bytes(),
|
||||
HashAlgo::Sha256,
|
||||
OathDigits::Six,
|
||||
6,
|
||||
None,
|
||||
)
|
||||
.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;
|
||||
|
||||
/// OathCredential stores one 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 transaction;
|
||||
|
||||
use constants::*;
|
||||
use oath_credential::*;
|
||||
use oath_credentialid::*;
|
||||
use oath_credential_id::*;
|
||||
use refreshable_oath_credential::*;
|
||||
use transaction::*;
|
||||
|
||||
|
@ -38,6 +50,7 @@ fn time_challenge(timestamp: Option<SystemTime>, period: Option<Duration>) -> [u
|
|||
.to_be_bytes()
|
||||
}
|
||||
|
||||
/// keeps track of transactions with a named YubiKey
|
||||
pub struct OathSession {
|
||||
version: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
|
@ -97,10 +110,6 @@ impl OathSession {
|
|||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if !self.locked {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let hmac = hmac_sha1(key, &chal);
|
||||
let random_chal = getrandom::u64()?.to_be_bytes();
|
||||
let data = &[
|
||||
|
@ -120,14 +129,13 @@ impl OathSession {
|
|||
self.locked = false;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Authentication)
|
||||
Err(Error::Unknown(
|
||||
"Unlocking session failed unexpectedly".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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 hmac = hmac_sha1(key, &random_chal);
|
||||
let data = &[
|
||||
|
@ -152,10 +160,6 @@ impl OathSession {
|
|||
}
|
||||
|
||||
pub fn unset_key(&mut self) -> Result<(), Error> {
|
||||
if self.locked {
|
||||
return Err(Error::Authentication);
|
||||
}
|
||||
|
||||
self.transaction_context.apdu(
|
||||
0,
|
||||
Instruction::SetCode as u8,
|
||||
|
@ -173,10 +177,6 @@ impl OathSession {
|
|||
old: CredentialIDData,
|
||||
new: CredentialIDData,
|
||||
) -> Result<CredentialIDData, Error> {
|
||||
if self.locked {
|
||||
return Err(Error::Authentication);
|
||||
}
|
||||
|
||||
self.require_version(vec![5, 3, 1])?;
|
||||
self.transaction_context.apdu(
|
||||
0,
|
||||
|
@ -189,10 +189,6 @@ impl OathSession {
|
|||
}
|
||||
|
||||
pub fn delete_code(&self, cred: OathCredential) -> Result<(), Error> {
|
||||
if self.locked {
|
||||
return Err(Error::Authentication);
|
||||
}
|
||||
|
||||
self.transaction_context.apdu(
|
||||
0,
|
||||
Instruction::Delete as u8,
|
||||
|
@ -208,13 +204,9 @@ impl OathSession {
|
|||
cred: OathCredential,
|
||||
secret: &[u8],
|
||||
algo: HashAlgo,
|
||||
digits: OathDigits,
|
||||
digits: u8,
|
||||
counter: Option<u32>,
|
||||
) -> Result<(), Error> {
|
||||
if self.locked {
|
||||
return Err(Error::Authentication);
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -226,7 +218,7 @@ impl OathSession {
|
|||
to_tlv(
|
||||
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(),
|
||||
]
|
||||
.concat(),
|
||||
|
@ -270,10 +262,6 @@ impl OathSession {
|
|||
cred: &OathCredential,
|
||||
timestamp_sys: Option<SystemTime>,
|
||||
) -> Result<OathCodeDisplay, Error> {
|
||||
if self.locked {
|
||||
return Err(Error::Authentication);
|
||||
}
|
||||
|
||||
if self.name != cred.device_id {
|
||||
return Err(Error::DeviceMismatch);
|
||||
}
|
||||
|
@ -308,10 +296,6 @@ impl OathSession {
|
|||
/// Read the OATH codes from the device, calculate TOTP codes that don't
|
||||
/// need touch
|
||||
pub fn calculate_oath_codes(&self) -> Result<Vec<RefreshableOathCredential>, Error> {
|
||||
if self.locked {
|
||||
return Err(Error::Authentication);
|
||||
}
|
||||
|
||||
let timestamp = SystemTime::now();
|
||||
// Request OATH codes from device
|
||||
let response = self.transaction_context.apdu_read_all(
|
||||
|
@ -347,10 +331,6 @@ impl OathSession {
|
|||
Ok(key_buffer)
|
||||
}
|
||||
pub fn list_oath_codes(&self) -> Result<Vec<CredentialIDData>, Error> {
|
||||
if self.locked {
|
||||
return Err(Error::Authentication);
|
||||
}
|
||||
|
||||
// Request OATH codes from device
|
||||
let response =
|
||||
self.transaction_context
|
||||
|
|
|
@ -14,14 +14,13 @@ pub enum Error {
|
|||
Pcsc(pcsc::Error),
|
||||
Parsing(String),
|
||||
DeviceMismatch,
|
||||
Authentication,
|
||||
Random(getrandom::Error),
|
||||
Version(Vec<u8>, Vec<u8>),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
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) {
|
||||
return Err(Self::Protocol(e));
|
||||
}
|
||||
|
@ -55,7 +54,6 @@ impl Display for Error {
|
|||
Self::Pcsc(error) => f.write_fmt(format_args!("{}", error)),
|
||||
Self::Parsing(msg) => f.write_str(msg),
|
||||
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::Version(ver, req) => f.write_fmt(format_args!(
|
||||
"Version requirement not met: is {}, required {}",
|
||||
|
|
Loading…
Add table
Reference in a new issue