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;
fn decode_secret(secret: &str) -> Result<Vec<u8>> {
let res = BASE32.decode(secret.as_bytes())?;
Ok(res)
}
pub fn is_valid(secret: &str) -> bool {
decode_secret(secret).is_ok()
}
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())
}
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 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)
}