1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
use anyhow::Result;
use data_encoding::BASE32;
use ring::hmac;
use std::convert::TryInto;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

pub static STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY";
pub static STEAM_DEFAULT_PERIOD: u32 = 30;
pub static STEAM_DEFAULT_DIGITS: u32 = 5;
pub static HOTP_DEFAULT_COUNTER: u32 = 1;
pub static DEFAULT_DIGITS: u32 = 6;
pub static TOTP_DEFAULT_PERIOD: u32 = 30;

mod algorithm;
pub use algorithm::Algorithm;

/// Code graciously taken from the rust-top crate.
/// https://github.com/TimDumol/rust-otp/blob/master/src/lib.rs

/// Decodes a secret (given as an RFC4648 base32-encoded ASCII string)
/// into a byte string. It fails if secret is not a valid Base32 string.
fn decode_secret(secret: &str) -> Result<Vec<u8>> {
    let res = BASE32.decode(secret.as_bytes())?;
    Ok(res)
}

/// Validates if `secret` is a valid Base32 String.
pub fn is_valid(secret: &str) -> bool {
    decode_secret(secret).is_ok()
}

/// Calculates the HMAC digest for the given secret and counter.
fn calc_digest(decoded_secret: &[u8], counter: u64, algorithm: Algorithm) -> hmac::Tag {
    let key = hmac::Key::new(algorithm.into(), decoded_secret);
    hmac::sign(&key, &counter.to_be_bytes())
}

/// Encodes the HMAC digest into a n-digit integer.
fn encode_digest(digest: &[u8]) -> Result<u32> {
    let offset = match digest.last() {
        Some(x) => *x & 0xf,
        None => anyhow::bail!("Invalid digest"),
    } as usize;
    let code_bytes: [u8; 4] = match digest[offset..offset + 4].try_into() {
        Ok(x) => x,
        Err(_) => anyhow::bail!("Invalid digest"),
    };
    let code = u32::from_be_bytes(code_bytes);
    Ok(code & 0x7fffffff)
}

/// Performs the [HMAC-based One-time Password Algorithm](http://en.wikipedia.org/wiki/HMAC-based_One-time_Password_Algorithm)
/// (HOTP) given an RFC4648 base32 encoded secret, and an integer counter.
pub fn hotp(secret: &str, counter: u64, algorithm: Algorithm, digits: u32) -> Result<u32> {
    let decoded = decode_secret(secret)?;
    let digest = encode_digest(calc_digest(decoded.as_slice(), counter, algorithm).as_ref())?;
    Ok(digest % 10_u32.pow(digits))
}

pub fn steam(secret: &str, counter: u64) -> Result<String> {
    let decoded = decode_secret(secret)?;
    let mut full_token =
        encode_digest(calc_digest(decoded.as_slice(), counter, Algorithm::SHA1).as_ref())?;

    let mut code = String::new();
    let total_chars = STEAM_CHARS.len() as u32;
    for _ in 0..STEAM_DEFAULT_DIGITS {
        let pos = full_token % total_chars;
        let charachter = STEAM_CHARS.chars().nth(pos as usize).unwrap();
        code.push(charachter);
        full_token /= total_chars;
    }
    Ok(code)
}

pub fn format(code: u32, digits: usize) -> String {
    let padded_code = format!("{:0width$}", code, width = digits);
    let mut formated_code = String::new();
    for (idx, ch) in padded_code.chars().enumerate() {
        if (digits - idx) % 3 == 0 && (digits - idx) != 0 && idx != 0 {
            formated_code.push(' ');
        }
        formated_code.push(ch);
    }
    formated_code
}

pub fn time_based_counter(period: u32) -> u64 {
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    timestamp / period as u64
}

pub fn remaining_time(period: u32) -> Duration {
    let period = period as u128 * 1000;
    let now: u128 = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis();
    let duration = period - now % period;
    Duration::from_millis(duration as u64)
}