use crate::core;
use crate::core::{deserialize_hex, serialize_hex};
use crate::token::Token;
use aes::cipher::{BlockEncrypt, KeyInit};
use aes::Aes128;
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
#[cfg(test)]
use mock_instant::thread_local::Instant;
#[cfg(not(test))]
use std::time::Instant;
#[derive(Serialize, Deserialize, Debug)]
pub struct Yubikey {
#[serde(serialize_with = "serialize_hex", deserialize_with = "deserialize_hex")]
pub public_id: [u8; 6],
#[serde(serialize_with = "serialize_hex", deserialize_with = "deserialize_hex")]
pub private_id: [u8; 6],
#[serde(serialize_with = "serialize_hex", deserialize_with = "deserialize_hex")]
pub aes_key: [u8; 16],
pub usage_counter: u16,
#[serde(skip, default = "default_session_counter")]
session_counter: u8,
#[serde(skip, default = "default_time")]
start_time: Instant,
}
fn default_session_counter() -> u8 {
1
}
fn default_time() -> Instant {
Instant::now()
}
fn get_random_bytes(length: u16) -> Vec<u8> {
let bytes: Vec<u8> = thread_rng()
.sample_iter(Uniform::from(0..255))
.take(length as usize)
.collect();
return bytes;
}
#[cfg(test)]
fn get_random_number() -> [u8; 2] {
return [0xd7, 0x55];
}
#[cfg(not(test))]
fn get_random_number() -> [u8; 2] {
thread_rng().gen()
}
impl Default for Yubikey {
fn default() -> Self {
Self {
public_id: get_random_bytes(6).try_into().unwrap(),
private_id: get_random_bytes(6).try_into().unwrap(),
aes_key: get_random_bytes(16).try_into().unwrap(),
usage_counter: 1,
session_counter: default_session_counter(),
start_time: default_time(),
}
}
}
impl Token for Yubikey {
const NAME: &'static str = "yubikey";
fn generate_password(&self) -> String {
core::to_modkex(&self.encrypted_password())
}
}
impl Yubikey {
fn encrypted_password(&self) -> [u8; 22] {
let plain_password = self.plain_password();
let basic = Aes128::new(&self.aes_key.into());
let mut block = plain_password.into();
basic.encrypt_block(&mut block);
let mut password: [u8; 22] = Default::default();
password[0..6].clone_from_slice(&self.public_id);
password[6..22].clone_from_slice(&block);
return password;
}
fn plain_password(&self) -> [u8; 16] {
let mut plain_password: [u8; 16] = Default::default();
plain_password[0..6].clone_from_slice(&self.private_id);
plain_password[6..8].clone_from_slice(&self.usage_counter.to_le_bytes());
plain_password[8..11].clone_from_slice(&self.get_timestamp());
plain_password[11..12].clone_from_slice(&[self.session_counter]);
plain_password[12..14].clone_from_slice(&get_random_number());
let checksum = core::get_crc(&plain_password[0..14]).to_le_bytes();
plain_password[14..16].clone_from_slice(&checksum);
plain_password
}
fn get_timestamp(&self) -> [u8; 3] {
let now = Instant::now();
let duration = now - self.start_time;
let number = (duration.as_secs_f64() * 8.0) as u64 % 0xFFFFFF;
let bytes = number.to_be_bytes();
return [bytes[7], bytes[6], bytes[5]];
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use mock_instant::thread_local::MockClock;
#[test]
fn test_timestamp() {
MockClock::set_time(Duration::ZERO);
let key: Yubikey = Default::default();
assert_eq!(key.get_timestamp(), [0, 0, 0]);
MockClock::advance(Duration::from_secs(2));
assert_eq!(key.get_timestamp(), [0x10, 0, 0]);
MockClock::advance(Duration::from_millis(150));
assert_eq!(key.get_timestamp(), [0x11, 0, 0]);
MockClock::advance(Duration::from_millis(50));
assert_eq!(key.get_timestamp(), [0x11, 0, 0]);
}
#[test]
fn test_timestamp_byte_order() {
MockClock::set_time(Duration::ZERO);
let key: Yubikey = Default::default();
MockClock::advance(Duration::from_secs(86402));
assert_eq!(key.get_timestamp(), [0x10, 0x8c, 0x0a]);
}
#[test]
fn error_deserializing_key_with_wrong_lengths() {
assert!(serde_json::from_str::<Yubikey>(
r#"{
"public_id": "2222c69234eeaa",
"private_id": "7af41898fca4",
"aes_key": "a629bc6efc4b6e3d3580a54d5b798cd0",
"usage_counter": 1
}"#,
)
.is_err());
assert!(serde_json::from_str::<Yubikey>(
r#"{
"public_id": "2222c69234ee",
"private_id": "7af41898fc",
"aes_key": "a629bc6efc4b6e3d3580a54d5b798cd0",
"usage_counter": 1
}"#,
)
.is_err());
assert!(serde_json::from_str::<Yubikey>(
r#"{
"public_id": "2222c69234ee",
"private_id": "7af41898fca",
"aes_key": "a629bc6efc4b6e3d3580a54d5b798cd0",
"usage_counter": 1
}"#,
)
.is_err());
assert!(serde_json::from_str::<Yubikey>(
r#"{
"public_id": "2222c69234eeaa",
"private_id": "7af41898fca4",
"aes_key": "a6",
"usage_counter": 1
}"#,
)
.is_err());
}
#[test]
fn password_regression() {
MockClock::set_time(Duration::ZERO);
let key: Yubikey = serde_json::from_str(
r#"{
"public_id": "2222c69234ee",
"private_id": "7af41898fca4",
"aes_key": "a629bc6efc4b6e3d3580a54d5b798cd0",
"usage_counter": 1
}"#,
)
.unwrap();
MockClock::advance(Duration::from_secs(86402));
assert_eq!(
hex::encode(key.plain_password()),
concat!("7af41898fca4", "0100", "108c0a", "01", "d755", "fa60")
);
assert_eq!(
hex::encode(key.encrypted_password()),
concat!("2222c69234ee", "a1caefcf7f427d5e69774875c4be8c28")
);
assert_eq!(
key.generate_password(),
concat!("ddddrhkdefuu", "lbrluvrvivfditguhkiifjigrfnujrdj")
);
}
}