otp/
totp.rs

1//! Provides RFC6238 compliant TOTP token generation.
2use std::time::{Duration, SystemTime};
3
4pub use crypto;
5pub use crypto::digest::Digest;
6pub use crypto::sha1::Sha1;
7
8use crate::config::TotpOptions;
9use crate::{TotpError, TotpResult};
10
11use serde::{Deserialize, Serialize};
12
13use super::secrets;
14
15static ALPHABET: base32::Alphabet = base32::Alphabet::Rfc4648 { padding: false };
16
17/// [RFC6238-timestep]: https://tools.ietf.org/html/rfc6238#section-5.2
18/// [RFC6238 recommended][RFC6238-timestep] time step duration of 30 seconds.
19pub const RFC6238_RECOMMENDED_TIMESTEP: Duration = Duration::from_secs(30);
20
21#[derive(Serialize, Deserialize, Debug, Clone)]
22pub enum TokenAlgorithm {
23    #[serde(rename = "sha1")]
24    TotpSha1,
25    #[cfg(feature = "rsa_stoken")]
26    #[serde(rename = "stoken")]
27    SToken,
28}
29
30impl Copy for TokenAlgorithm {}
31
32#[allow(dead_code)]
33trait AsDigest {
34    fn as_digest(&self) -> Box<dyn Digest>;
35}
36
37impl AsDigest for TokenAlgorithm {
38    fn as_digest(&self) -> Box<dyn Digest> {
39        Box::new(match self {
40            TokenAlgorithm::TotpSha1 => Sha1::new(),
41            #[cfg(feature = "rsa_stoken")]
42            TokenAlgorithm::SToken => unreachable!("SToken cannot be used as a digest method"),
43        })
44    }
45}
46
47/// Runs a standard TOTP for the provided config, looking up secrets using []()
48///
49/// # Examples
50/// ```rust
51/// use otp::config::TotpOptions;
52/// use otp::totp::TokenAlgorithm;
53/// use otp::totp::standard_totp;
54/// let options = TotpOptions::new_config_stored_secret(
55///   "A SECRET".to_string(),
56///   TokenAlgorithm::TotpSha1);
57///
58/// let  code = standard_totp("test", &options).expect("Failed to generate a TOTP code");
59///
60/// assert_eq!(code.len(), 6);
61///
62/// const BASE_10: u32 = 10;
63/// assert!(code.chars().all(|c| c.is_digit(BASE_10)))
64///
65/// ```
66pub fn standard_totp(name: &str, options: &TotpOptions) -> TotpResult<String> {
67    let secret = secrets::get_secret(name, options)?;
68    generate_sha1_code(secret)
69}
70
71/// Cleans a base32 secret by removing spaces and making sure it's upper-cased.
72pub fn clean_secret(secret: &str) -> String {
73    secret.replace(' ', "").to_uppercase()
74}
75
76/// Generate a SHA1 TOTP code
77///
78/// # Examples
79/// ```rust
80/// use otp::totp::generate_sha1_code;
81/// let  code = generate_sha1_code("A BASE 32 SECRET".to_string()).expect("Failed to generate a TOTP code");
82///
83/// assert_eq!(code.len(), 6);
84///
85/// const BASE_10: u32 = 10;
86/// assert!(code.chars().all(|c| c.is_digit(BASE_10)))
87///
88/// ```
89pub fn generate_sha1_code(secret: String) -> TotpResult<String> {
90    let now = SystemTime::now();
91    let seconds: Duration = now
92        .duration_since(SystemTime::UNIX_EPOCH)
93        .expect("Can't get time since UNIX_EPOCH?");
94
95    let clean_secret = secret.replace(' ', "").to_uppercase();
96    let secret = base32::decode(ALPHABET, &clean_secret)
97        .ok_or(TotpError("Failed to decode secret from base32"))?;
98
99    let algo_sha1 = Sha1::new();
100    totp(&secret, seconds, RFC6238_RECOMMENDED_TIMESTEP, 6, algo_sha1)
101}
102
103const DIGITS_MODULUS: [u32; 9] = [
104    1u32,           // 0
105    10u32,          // 1
106    100u32,         // 2
107    1000u32,        // 3
108    10_000u32,      // 4
109    100_000u32,     // 5
110    1_000_000u32,   // 6
111    10_000_000u32,  // 7
112    100_000_000u32, // 8
113];
114
115/// Generate a RFC6238 TOTP code using the supplied secret, time, time step size, output length, and algorithm
116///
117/// # Examples
118/// ```rust
119/// // This SHA1 example is from the RFC: https://tools.ietf.org/html/rfc6238#appendix-B
120/// use std::time::Duration;
121/// use otp::totp::{Sha1, RFC6238_RECOMMENDED_TIMESTEP, totp};
122/// let secret = b"12345678901234567890";
123/// let time_since_epoch = Duration::from_secs(59);
124/// let output_length = 8;
125/// let algo = Sha1::new();
126///
127/// let totp_code = totp(secret, time_since_epoch, RFC6238_RECOMMENDED_TIMESTEP, 8, algo)
128///   .expect("Failed to generate TOTP code");
129///
130/// assert_eq!(totp_code, "94287082");
131/// ```
132pub fn totp<D>(
133    secret: &[u8],
134    time_since_epoch: Duration,
135    time_step: Duration,
136    length: usize,
137    algo: D,
138) -> TotpResult<String>
139where
140    D: Digest,
141{
142    use byteorder::{BigEndian, ByteOrder};
143    use crypto::{hmac::Hmac, mac::Mac};
144
145    let mut buf: [u8; 8] = [0; 8];
146    BigEndian::write_u64(&mut buf, time_since_epoch.as_secs() / time_step.as_secs());
147
148    let mut hmac1 = Hmac::new(algo, secret);
149    hmac1.input(&buf);
150    let mac_result = hmac1.result();
151    let signature = mac_result.code();
152
153    let modulus: u32 = DIGITS_MODULUS[length];
154
155    let code: u32 = truncate(signature) % modulus;
156
157    // zero pad using format fills
158    // https://doc.rust-lang.org/std/fmt/#fillalignment
159    // https://doc.rust-lang.org/std/fmt/#width
160    Ok(format!("{:0>width$}", code, width = length))
161}
162
163fn truncate(signature: &[u8]) -> u32 {
164    let offset: usize = (signature[signature.len() - 1] & 0xF).into();
165    let bytes = &signature[offset..offset + std::mem::size_of::<u32>()];
166
167    let high = u32::from(bytes[0]);
168    let high_num = (high & 0x7F) << 24;
169
170    let mid = u32::from(bytes[1]);
171    let mid_num = mid << 16;
172
173    let lower = u32::from(bytes[2]);
174    let lower_num = lower << 8;
175
176    let bottom = u32::from(bytes[3]);
177
178    high_num | mid_num | lower_num | bottom
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    // Example code from
186    // https://tools.ietf.org/html/rfc6238#appendix-A
187    fn rfc6238_test<D: Digest>(
188        time_since_epoch: Duration,
189        digest: D,
190        expected_code: &str,
191    ) -> TotpResult<()> {
192        const RFC_SECRET_SEED: &[u8] = b"12345678901234567890";
193
194        // Need to seed with the proper number of bytes (sha1 = 20 bytes, sha256 = 32, sha512 = 64)
195        let secret: Vec<u8> = std::iter::repeat(RFC_SECRET_SEED)
196            .flatten()
197            .take(digest.output_bytes())
198            .cloned()
199            .collect();
200
201        let code = totp(
202            &secret,
203            time_since_epoch,
204            RFC6238_RECOMMENDED_TIMESTEP,
205            8,
206            digest,
207        )?;
208
209        assert_eq!(code, expected_code);
210
211        Ok(())
212    }
213
214    #[cfg(test)]
215    #[test]
216    fn rfc6238_sha1_tests() -> TotpResult<()> {
217        // test vectors from the RFC
218        // https://tools.ietf.org/html/rfc6238#appendix-B
219
220        fn algo() -> impl Digest {
221            Sha1::new()
222        }
223
224        rfc6238_test(Duration::from_secs(59), algo(), "94287082")?;
225        rfc6238_test(Duration::from_secs(1_111_111_109), algo(), "07081804")?;
226        rfc6238_test(Duration::from_secs(1_111_111_111), algo(), "14050471")?;
227        rfc6238_test(Duration::from_secs(1_234_567_890), algo(), "89005924")?;
228        rfc6238_test(Duration::from_secs(2_000_000_000), algo(), "69279037")?;
229        rfc6238_test(Duration::from_secs(20_000_000_000), algo(), "65353130")?;
230
231        Ok(())
232    }
233
234    #[cfg(test)]
235    #[test]
236    fn rfc6238_sha256_tests() -> TotpResult<()> {
237        // test vectors from the RFC
238        // https://tools.ietf.org/html/rfc6238#appendix-B
239
240        fn algo() -> impl Digest {
241            crypto::sha2::Sha256::new()
242        }
243
244        rfc6238_test(Duration::from_secs(59), algo(), "46119246")?;
245        rfc6238_test(Duration::from_secs(1_111_111_109), algo(), "68084774")?;
246        rfc6238_test(Duration::from_secs(1_111_111_111), algo(), "67062674")?;
247        rfc6238_test(Duration::from_secs(1_234_567_890), algo(), "91819424")?;
248        rfc6238_test(Duration::from_secs(2_000_000_000), algo(), "90698825")?;
249        rfc6238_test(Duration::from_secs(20_000_000_000), algo(), "77737706")?;
250
251        Ok(())
252    }
253
254    #[cfg(test)]
255    #[test]
256    fn rfc6238_sha512_tests() -> TotpResult<()> {
257        // test vectors from the RFC
258        // https://tools.ietf.org/html/rfc6238#appendix-B
259
260        fn algo() -> impl Digest {
261            crypto::sha2::Sha512::new()
262        }
263
264        rfc6238_test(Duration::from_secs(59), algo(), "90693936")?;
265        rfc6238_test(Duration::from_secs(1_111_111_109), algo(), "25091201")?;
266        rfc6238_test(Duration::from_secs(1_111_111_111), algo(), "99943326")?;
267        rfc6238_test(Duration::from_secs(1_234_567_890), algo(), "93441116")?;
268        rfc6238_test(Duration::from_secs(2_000_000_000), algo(), "38618901")?;
269        rfc6238_test(Duration::from_secs(20_000_000_000), algo(), "47863826")?;
270
271        Ok(())
272    }
273}