totp_lite/
lib.rs

1//! A simple, correct TOTP library.
2//!
3//! Time-based One-time Passwords are a useful way to authenticate a client,
4//! since a valid password expires long before it could ever be guessed by an
5//! attacker. This library provides an implementation of TOTP that matches its
6//! specification [RFC6238], along with a simple interface.
7//!
8//! # Usage
9//!
10//! The `totp` function is likely what you need. It uses the default time step
11//! of 30 seconds and produces 8 digits of output:
12//!
13//! ```
14//! use std::time::{SystemTime, UNIX_EPOCH};
15//! use totp_lite::{totp, Sha512};
16//!
17//! // Negotiated between you and the authenticating service.
18//! let password: &[u8] = b"secret";
19//!
20//! // The number of seconds since the Unix Epoch.
21//! let seconds: u64 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
22//!
23//! // Specify the desired Hash algorithm via a type parameter.
24//! // `Sha1` and `Sha256` are also available.
25//! let result: String = totp::<Sha512>(password, seconds);
26//! assert_eq!(8, result.len());
27//! ```
28//!
29//! For full control over how the algorithm is configured, consider
30//! `totp_custom`.
31//!
32//! # Resources
33//! - [RFC6238: TOTP][RFC6238]
34//! - [RFC6238 Errata](https://www.rfc-editor.org/errata_search.php?rfc=6238)
35//!
36//! [RFC6238]: https://tools.ietf.org/html/rfc6238
37
38#![doc(html_root_url = "https://docs.rs/totp-lite/2.0.1")]
39
40use digest::{
41    block_buffer::Eager,
42    core_api::{BlockSizeUser, BufferKindUser, CoreProxy, FixedOutputCore, UpdateCore},
43    generic_array::typenum::{IsLess, Le, NonZero, U256},
44    FixedOutput, HashMarker, Update,
45};
46use hmac::{Hmac, Mac};
47pub use sha1::Sha1;
48pub use sha2::{Sha256, Sha512};
49
50/// 30 seconds.
51pub const DEFAULT_STEP: u64 = 30;
52
53/// 8 digits of output.
54pub const DEFAULT_DIGITS: u32 = 8;
55
56/// Produce a Time-based One-time Password with default settings.
57///
58/// ```
59/// use std::time::{SystemTime, UNIX_EPOCH};
60/// use totp_lite::{totp, Sha512};
61///
62/// // Negotiated between you and the authenticating service.
63/// let password: &[u8] = b"secret";
64///
65/// // The number of seconds since the Unix Epoch.
66/// let seconds: u64 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
67///
68/// // Specify the desired Hash algorithm via a type parameter.
69/// // `Sha1` and `Sha256` are also available.
70/// let result: String = totp::<Sha512>(password, seconds);
71/// assert_eq!(8, result.len());
72/// assert_eq!("71788658", totp::<Sha512>(password, 1234567890)); // 2009 February 13.
73/// ```
74pub fn totp<H>(secret: &[u8], time: u64) -> String
75where
76    H: Update + FixedOutput + CoreProxy,
77    H::Core: HashMarker
78        + UpdateCore
79        + FixedOutputCore
80        + BufferKindUser<BufferKind = Eager>
81        + Default
82        + Clone,
83    <H::Core as BlockSizeUser>::BlockSize: IsLess<U256>,
84    Le<<H::Core as BlockSizeUser>::BlockSize, U256>: NonZero,
85{
86    totp_custom::<H>(DEFAULT_STEP, DEFAULT_DIGITS, secret, time)
87}
88
89/// Produce a Time-based One-time Password with full control over algorithm parameters.
90///
91/// ```
92/// use std::time::{SystemTime, UNIX_EPOCH};
93/// use totp_lite::{totp_custom, Sha512, DEFAULT_STEP};
94///
95/// // Negotiated between you and the authenticating service.
96/// let password: &[u8] = b"secret";
97///
98/// // The number of seconds since the Unix Epoch.
99/// let seconds: u64 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
100///
101/// // Uses the default step of 30 seconds, but asks for 10 result digits instead.
102/// let result: String = totp_custom::<Sha512>(DEFAULT_STEP, 10, password, seconds);
103/// assert_eq!(10, result.len());
104/// ```
105pub fn totp_custom<H>(step: u64, digits: u32, secret: &[u8], time: u64) -> String
106where
107    H: Update + FixedOutput + CoreProxy,
108    H::Core: HashMarker
109        + UpdateCore
110        + FixedOutputCore
111        + BufferKindUser<BufferKind = Eager>
112        + Default
113        + Clone,
114    <H::Core as BlockSizeUser>::BlockSize: IsLess<U256>,
115    Le<<H::Core as BlockSizeUser>::BlockSize, U256>: NonZero,
116{
117    // Hash the secret and the time together.
118    let mut mac = <Hmac<H> as Mac>::new_from_slice(secret).unwrap();
119    <Hmac<H> as Update>::update(&mut mac, &to_bytes(time / step));
120    let hash: &[u8] = &mac.finalize().into_bytes();
121
122    // Magic from the RFC.
123    let offset: usize = (hash.last().unwrap() & 0xf) as usize;
124    let binary: u64 = (((hash[offset] & 0x7f) as u64) << 24)
125        | ((hash[offset + 1] as u64) << 16)
126        | ((hash[offset + 2] as u64) << 8)
127        | (hash[offset + 3] as u64);
128
129    format!("{:01$}", binary % (10_u64.pow(digits)), digits as usize)
130}
131
132/// Convert a `u64` into its individual bytes.
133fn to_bytes(n: u64) -> [u8; 8] {
134    let mask = 0x00000000000000ff;
135    let mut bytes: [u8; 8] = [0; 8];
136    (0..8).for_each(|i| bytes[7 - i] = (mask & (n >> (i * 8))) as u8);
137    bytes
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn to_bytes_test() {
146        assert_eq!(vec![0, 0, 0, 0, 0, 0, 0, 1], to_bytes(59 / DEFAULT_STEP));
147        assert_eq!(
148            vec![0, 0, 0, 0, 0x02, 0x35, 0x23, 0xec],
149            to_bytes(1111111109 / DEFAULT_STEP)
150        );
151        assert_eq!(
152            vec![0, 0, 0, 0, 0x27, 0xbc, 0x86, 0xaa],
153            to_bytes(20000000000 / DEFAULT_STEP)
154        );
155    }
156
157    #[test]
158    fn variable_length() {
159        let secret: &[u8] = b"12345678901234567890123456789012";
160        assert_eq!(
161            "2102975832",
162            totp_custom::<Sha256>(DEFAULT_STEP, 10, secret, 100)
163        );
164        assert_eq!(
165            "975832",
166            totp_custom::<Sha256>(DEFAULT_STEP, 6, secret, 100)
167        );
168    }
169
170    #[test]
171    fn totp1_tests() {
172        let secret: &[u8] = b"12345678901234567890";
173        assert_eq!(20, secret.len());
174
175        let pairs = vec![
176            ("94287082", 59),
177            ("07081804", 1111111109),
178            ("14050471", 1111111111),
179            ("89005924", 1234567890),
180            ("69279037", 2000000000),
181            ("65353130", 20000000000),
182        ];
183
184        pairs.into_iter().for_each(|(expected, time)| {
185            assert_eq!(expected, totp::<Sha1>(secret, time));
186        });
187    }
188
189    #[test]
190    fn totp256_tests() {
191        let secret: &[u8] = b"12345678901234567890123456789012";
192        assert_eq!(32, secret.len());
193
194        let pairs = vec![
195            ("46119246", 59),
196            ("68084774", 1111111109),
197            ("67062674", 1111111111),
198            ("91819424", 1234567890),
199            ("90698825", 2000000000),
200            ("77737706", 20000000000),
201        ];
202
203        pairs.into_iter().for_each(|(expected, time)| {
204            assert_eq!(expected, totp::<Sha256>(secret, time));
205        });
206    }
207
208    #[test]
209    fn totp512_tests() {
210        let secret: &[u8] = b"1234567890123456789012345678901234567890123456789012345678901234";
211        assert_eq!(64, secret.len());
212
213        let pairs = vec![
214            ("90693936", 59),
215            ("25091201", 1111111109),
216            ("99943326", 1111111111),
217            ("93441116", 1234567890),
218            ("38618901", 2000000000),
219            ("47863826", 20000000000),
220        ];
221
222        pairs.into_iter().for_each(|(expected, time)| {
223            assert_eq!(expected, totp::<Sha512>(secret, time));
224        });
225    }
226}