ootp/
totp.rs

1use crate::constants::{DEFAULT_ALGORITHM, DEFAULT_DIGITS, DEFAULT_PERIOD};
2use crate::hotp::{CheckOption, Hotp, MakeOption};
3use hmacsha::ShaTypes;
4use std::time::SystemTime;
5
6fn create_counter(period: u64) -> u64 {
7    SystemTime::now()
8        .duration_since(SystemTime::UNIX_EPOCH)
9        .unwrap()
10        .as_secs()
11        / period
12}
13
14/// The TOTP is a HOTP-based one-time password algorithm, with a time value as moving factor.
15///
16/// It takes four parameter. An `Hotp` istance, the desired number of digits, a time period and the SHA algorithm.
17pub struct Totp<'a> {
18    pub hotp: Hotp<'a>,
19    pub digits: u32,
20    pub period: u64,
21    pub algorithm: &'a ShaTypes,
22}
23/// The Options for the TOTP's `make` function.
24#[derive(Clone, Copy)]
25pub enum CreateOption<'a> {
26    /// The default case. `Period = 30` seconds and `Digits = 6`.
27    Default,
28    /// Specify the desired number of `Digits`.
29    Digits(u32),
30    /// Specify the desired time `Period`.
31    Period(u64),
32    /// Specify both the desired time `Period` and the number of `Digits`.
33    Full {
34        digits: u32,
35        period: u64,
36        algorithm: &'a ShaTypes,
37    },
38    /// Specify the SHA algorihm
39    Algorithm(&'a ShaTypes),
40}
41
42impl<'a> Totp<'a> {
43    /// TOTP instance "private" constructor
44    const fn new(hotp: Hotp<'a>, digits: u32, period: u64, algorithm: &'a ShaTypes) -> Self {
45        Self {
46            hotp,
47            digits,
48            period,
49            algorithm,
50        }
51    }
52
53    /// TOTP instance constructor
54    pub const fn secret(secret: &'a str, option: CreateOption<'a>) -> Totp<'a> {
55        let hotp = Hotp::new(secret);
56        let (digits, period, algorithm) = match option {
57            CreateOption::Default => (DEFAULT_DIGITS, DEFAULT_PERIOD, DEFAULT_ALGORITHM),
58            CreateOption::Digits(digits) => (digits, DEFAULT_PERIOD, DEFAULT_ALGORITHM),
59            CreateOption::Period(period) => (DEFAULT_DIGITS, period, DEFAULT_ALGORITHM),
60            CreateOption::Full {
61                digits,
62                period,
63                algorithm,
64            } => (digits, period, algorithm),
65            CreateOption::Algorithm(algorithm) => (DEFAULT_DIGITS, DEFAULT_PERIOD, algorithm),
66        };
67        Totp::new(hotp, digits, period, algorithm)
68    }
69    /**
70    This function returns a string of the one-time password
71
72    # Example
73
74    ```rust
75    use ootp::totp::{Totp, CreateOption};
76
77    let secret = "A strong shared secrett";
78    let totp = Totp::secret(
79        secret,
80        CreateOption::Default
81    );
82
83    let otp = totp.make(); // Generate a one-time password
84    println!("{}", otp); // Print the one-time password
85    ```
86
87    */
88
89    pub fn make(&self) -> String {
90        self.hotp.make(MakeOption::Full {
91            counter: create_counter(self.period),
92            digits: self.digits,
93            algorithm: self.algorithm,
94        })
95    }
96
97    /**
98    This function returns a string of the one-time password, valid a `period` from `time` seconds since the UNIX epoch
99
100    # Example
101
102    ```rust
103    use ootp::totp::{Totp, CreateOption};
104
105    let secret = "A strong shared secrett";
106    let totp = Totp::secret(
107        secret,
108        CreateOption::Default
109    );
110
111    let otp = totp.make_time(59); // Generate a one-time password, valid a `DEFAULT_PERIOD from `59` seconds since the UNIX epoch
112    println!("{}", otp); // Print the one-time password
113    ```
114
115    */
116    pub fn make_time(&self, time: u64) -> String {
117        self.hotp.make(MakeOption::Full {
118            counter: time / self.period,
119            digits: self.digits,
120            algorithm: self.algorithm,
121        })
122    }
123    /**
124    Returns a boolean indicating if the one-time password is valid.
125
126    # Example #1
127
128    ```
129    use ootp::totp::{Totp, CreateOption};
130
131    let secret = "A strong shared secret";
132    let totp = Totp::secret(
133        secret,
134        CreateOption::Default
135    );
136    let otp = totp.make(); // Generate a one-time password
137    let check = totp.check(otp.as_str(), None);
138    ```
139
140    # Example #2
141
142    ```
143    use ootp::totp::{Totp, CreateOption};
144
145    let secret = "A strong shared secret";
146    let totp = Totp::secret(
147        secret,
148        CreateOption::Digits(8)
149    );
150    let otp = totp.make(); // Generate a one-time password
151    let check = totp.check(otp.as_str(), Some(42));
152    ```
153    */
154    pub fn check(&self, otp: &str, breadth: Option<u64>) -> bool {
155        self.hotp.check(
156            otp,
157            CheckOption::Full {
158                counter: create_counter(self.period),
159                breadth: breadth.unwrap_or(DEFAULT_PERIOD),
160                algorithm: self.algorithm,
161            },
162        )
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::{CreateOption, Totp};
169    use crate::constants::{self, DEFAULT_DIGITS};
170
171    #[test]
172    fn it_works() {
173        let secret = "A strong shared secret";
174        let totp = Totp::secret(secret, CreateOption::Default);
175        let code = totp.make();
176        assert_eq!(code.len(), DEFAULT_DIGITS as usize);
177    }
178
179    /// Taken from [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238#appendix-B)
180    #[test]
181    fn make_test_correcteness() {
182        let secret = "12345678901234567890";
183        let totp = Totp::secret(secret, CreateOption::Digits(8));
184        let code = totp.make_time(59);
185        assert_eq!(code, "94287082");
186        let code = totp.make_time(1_111_111_109);
187        assert_eq!(code, "07081804");
188        let code = totp.make_time(1_111_111_111);
189        assert_eq!(code, "14050471");
190        let code = totp.make_time(1_234_567_890);
191        assert_eq!(code, "89005924");
192        let code = totp.make_time(2_000_000_000);
193        assert_eq!(code, "69279037");
194        let code = totp.make_time(20_000_000_000);
195        assert_eq!(code, "65353130");
196    }
197
198    /// Taken from [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238#appendix-B)
199    /// Errata for [RFC 6238]](https://www.rfc-editor.org/errata_search.php?rfc=6238&rec_status=0)
200    #[test]
201    fn make_test_correcteness_sha256() {
202        let secret = "12345678901234567890123456789012";
203        let totp = Totp::secret(
204            secret,
205            CreateOption::Full {
206                digits: 8,
207                period: constants::DEFAULT_PERIOD,
208                algorithm: &hmacsha::ShaTypes::Sha2_256,
209            },
210        );
211        let code = totp.make_time(59);
212        assert_eq!(code, "46119246");
213        let code = totp.make_time(1_111_111_109);
214        assert_eq!(code, "68084774");
215        let code = totp.make_time(1_111_111_111);
216        assert_eq!(code, "67062674");
217        let code = totp.make_time(1_234_567_890);
218        assert_eq!(code, "91819424");
219        let code = totp.make_time(2_000_000_000);
220        assert_eq!(code, "90698825");
221        let code = totp.make_time(20_000_000_000);
222        assert_eq!(code, "77737706");
223    }
224
225    #[test]
226    fn check_test() {
227        let secret = "A strong shared secret";
228        let totp = Totp::secret(secret, CreateOption::Default);
229        let code = totp.make();
230        assert!(totp.check(code.as_str(), None))
231    }
232
233    #[test]
234    fn rapid_make_test() {
235        let secret = "A strong shared secret";
236        let totp = Totp::secret(secret, CreateOption::Default);
237        let code1 = totp.make();
238        let code2 = totp.make();
239        assert!(totp.check(code1.as_str(), None));
240        assert!(totp.check(code2.as_str(), None));
241        assert_eq!(code1, code2);
242    }
243}