doc: document constants
Some checks are pending
CI / Check (push) Waiting to run
CI / Test Suite (push) Waiting to run
CI / Rustfmt (push) Waiting to run
CI / Clippy (push) Waiting to run
CI / cargo-deny (push) Waiting to run

This commit is contained in:
Grimmauld 2025-02-15 23:01:46 +01:00
parent c596cfbcd9
commit b7b4e79c14
No known key found for this signature in database
5 changed files with 59 additions and 70 deletions

View file

@ -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),
})
}
}

View file

@ -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();

View file

@ -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

View file

@ -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 {}",