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
mod algorithm;
pub mod backup;
mod hotp;
mod otp_method;
mod otp_uri;
mod steam;
mod totp;

use anyhow::{anyhow, Result};
use ring::hmac;
use std::convert::TryInto;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

pub use algorithm::Algorithm;
pub use backup::Account;
pub use hotp::{HOTP, HOTP_DEFAULT_COUNTER};
pub use otp_method::OTPMethod;
pub use otp_uri::OTPUri;
pub use steam::{Steam, STEAM_DEFAULT_DIGITS, STEAM_DEFAULT_PERIOD};
pub use totp::{TOTP, TOTP_DEFAULT_PERIOD};

pub const DEFAULT_DIGITS: u32 = 6;

pub trait OTP {
    fn compute(&self, counter: u64) -> Result<String>;
}

/// 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>> {
    // The buffer should have a lenght of secret.len() * 5 / 8.
    let size = secret.len();
    let mut output_buffer = std::iter::repeat(0).take(size).collect::<Vec<u8>>();
    let ret = binascii::b32decode(secret.as_bytes(), &mut output_buffer)
        .map_err(|_| anyhow!("Invalid Input"))?;

    Ok(ret.to_vec())
}

/// 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.
pub(crate) 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.
pub(crate) 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)
}

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