twoken 0.4.0

Generate One-Time Passwords from stored token secrets.
Documentation
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]);

        // timestamp should increment every 125 milliseconds
        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()),
            //          private id   usage    time session random checksum
            concat!("7af41898fca4", "0100", "108c0a", "01", "d755", "fa60")
        );

        assert_eq!(
            hex::encode(key.encrypted_password()),
            //           public id                  encrypted password
            concat!("2222c69234ee", "a1caefcf7f427d5e69774875c4be8c28")
        );

        // finally, the modhex result
        assert_eq!(
            key.generate_password(),
            concat!("ddddrhkdefuu", "lbrluvrvivfditguhkiifjigrfnujrdj")
        );
    }
}