lugnut/
lib.rs

1use hmac::{crypto_mac, Hmac, Mac, NewMac};
2use rand;
3use sha1::Sha1;
4use sha2::{Sha256, Sha512};
5use thiserror::Error;
6use url::form_urlencoded::byte_serialize;
7
8type HmacSha1 = Hmac<Sha1>;
9type HmacSha256 = Hmac<Sha256>;
10type HmacSha512 = Hmac<Sha512>;
11
12mod hotp;
13mod totp;
14
15pub use totp::Totp;
16pub use hotp::Hotp;
17
18/// GenerationError enumerates all possible errors returned by this library.
19#[derive(Error, Debug)]
20pub enum GenerationError {
21    #[error("Invalid Key Length")]
22    InvalidKeyLength(#[from] crypto_mac::InvalidKeyLength),
23    #[error("Failed to generate One-Time Password")]
24    FailedToGenerateOTP(),
25}
26
27enum HmacFunction<A, B, C> {
28    Sha1(A),
29    Sha256(B),
30    Sha512(C),
31}
32
33pub enum Algorithm {
34    Sha1,
35    Sha256,
36    Sha512,
37}
38
39static CHAR_SET: [char; 62] = [
40    '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
41    'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
42    'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
43    'v', 'w', 'x', 'y', 'z',
44];
45static SYMBOL_SET: [char; 22] = [
46    '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '<', '>', '?', '/', '[', ']', '{', '}', ',',
47    '.', ':', ';',
48];
49
50/// Applys a specified keyed hashing function (hmac).
51///
52/// # Arguments
53///
54/// * `secret` - A string of the secret
55/// * `counter` - The counter to hash
56/// * `algorithm` - The preferred algorithm
57///
58/// # Examples
59///
60/// ```
61/// use lugnut::{ digest, Algorithm };
62/// let hash = digest("My secret".to_string(), 5000, Algorithm::Sha1);
63///
64pub fn digest(
65    secret: String,
66    counter: u128,
67    algorithm: Algorithm,
68) -> std::result::Result<Vec<u8>, GenerationError> {
69    let mac = get_hmac(secret, algorithm)?;
70
71    // Convert the counter into a u8 array of base16 values
72    let mut buf = vec![0; 8];
73    let mut tmp = counter;
74    for i in 0..8 {
75        buf[7 - i] = (tmp & 0xff) as u8;
76        tmp = tmp >> 8;
77    }
78
79    // Unwrap enum and apply the hmac alg
80    Ok(match mac {
81        HmacFunction::Sha1(mut _mac) => {
82            _mac.update(&buf);
83            _mac.finalize().into_bytes().to_vec()
84        }
85        HmacFunction::Sha256(mut _mac) => {
86            _mac.update(&buf);
87            _mac.finalize().into_bytes().to_vec()
88        }
89        HmacFunction::Sha512(mut _mac) => {
90            _mac.update(&buf);
91            _mac.finalize().into_bytes().to_vec()
92        }
93    })
94}
95
96/// Default layer to generate a secret key in ASCII representations
97///
98/// # Examples
99///
100/// ```
101/// use lugnut::{ generate_secret };
102/// let secret_key = generate_secret();
103/// ```
104pub fn generate_secret() -> String {
105    generate_secret_default(None, None)
106}
107
108/// Length defining layer to generate a secret key in ASCII representation
109///
110/// # Examples
111///
112/// ```
113/// use lugnut::{ generate_sized_secret };
114/// let secret_key = generate_sized_secret(100);
115/// ```
116pub fn generate_sized_secret(length: u32) -> String {
117    generate_secret_default(Some(length), None)
118}
119
120/// Symbol defining layer to generate a secret key in ASCII representation
121///
122/// # Examples
123///
124/// ```
125/// use lugnut::{ generate_secret_without_symbols };
126/// let secret_key = generate_secret_without_symbols();
127/// ```
128pub fn generate_secret_without_symbols() -> String {
129    generate_secret_default(None, Some(false))
130}
131
132/// Symbol and length defining layer to generate a secret key in ASCII representation
133///
134/// # Examples
135///
136/// ```
137/// use lugnut::{ generate_secret_without_symbols };
138/// let secret_key = generate_secret_without_symbols();
139/// ```
140pub fn generate_sized_secret_without_symbols(length: u32) -> String {
141    generate_secret_default(Some(length), Some(true))
142}
143
144pub fn get_otp_auth_url() {}
145
146/// This section works to fill up the unsigned 32 bit number by:
147/// 1.  Taking the 8 bits at the offset from the digest, AND'ing with 0x7f so that we can ignore the sign bit
148/// and then bit shifting 24 to the left to fill the most significant bits.
149/// 2.  Taking the next 8 bits from the digest at (offset + 1), AND'ing with 0xff to get the set bits, shifting 16 to fill
150/// the next 8 significant bits.
151/// 3.  Same as (2.) but taking the bits from (offset + 2)
152/// 4.  Same as (2.) but taking the bits from (offset + 3)
153/// 5.  OR'ing each of these u32 so that we collapse all of the set bits into one u32
154#[doc(hidden)]
155fn generate_otp(
156    digits: u32,
157    digest_hash: Vec<u8>,
158) -> std::result::Result<String, GenerationError> {
159    let offset = if let Some(o) = digest_hash.last() {
160        o & 0xf
161    } else {
162        0
163    };
164
165    let no_offset = if let Some(o) = digest_hash.get(offset as usize) {
166        u32::from(o.clone() & 0x7f) << 24
167    } else {
168        0
169    };
170    let one_offset = if let Some(o) = digest_hash.get((offset + 1) as usize) {
171        u32::from(o.clone() & 0xff) << 16
172    } else {
173        0
174    };
175    let two_offset = if let Some(o) = digest_hash.get((offset + 2) as usize) {
176        u32::from(o.clone() & 0xff) << 8
177    } else {
178        0
179    };
180    let three_offset = if let Some(o) = digest_hash.get((offset + 3) as usize) {
181        u32::from(o.clone() & 0xff)
182    } else {
183        0
184    };
185    let code = no_offset | one_offset | two_offset | three_offset;
186
187    if code == 0 {
188        // This is very unlikely to happen, but as a precaution we will return an Err
189        Err(GenerationError::FailedToGenerateOTP())
190    } else {
191        let padded_string = format!("{:0>width$}", code.to_string(), width = digits as usize);
192        Ok(
193            (&padded_string[(padded_string.len() - digits as usize)..padded_string.len()])
194                .to_string(),
195        )
196    }
197}
198
199#[doc(hidden)]
200fn verify_delta(
201    token: String,
202    counter: u128,
203    digits: u32,
204    window: u64,
205    digest_hash: Vec<u8>,
206) -> std::result::Result<bool, GenerationError> {
207    if token.len() as u32 != digits {
208        return Ok(false);
209    }
210
211    for _ in counter..=counter + window as u128 {
212        let test_otp = generate_otp(digits, digest_hash.clone())?;
213        if test_otp == token {
214            return Ok(true);
215        }
216    }
217
218    // Default false
219    Ok(false)
220}
221
222#[doc(hidden)]
223fn generate_secret_default(length: Option<u32>, symbols: Option<bool>) -> String {
224    let defined_symbols = if let Some(s) = symbols { s } else { true };
225    let defined_length = if let Some(l) = length { l } else { 32 };
226    generate_secret_ascii(defined_length, defined_symbols)
227}
228
229#[doc(hidden)]
230fn get_hmac(
231    secret: String,
232    algorithm: Algorithm,
233) -> std::result::Result<HmacFunction<HmacSha1, HmacSha256, HmacSha512>, GenerationError> {
234    Ok(match algorithm {
235        Algorithm::Sha1 => HmacFunction::Sha1(HmacSha1::new_varkey(secret.as_bytes())?),
236        Algorithm::Sha256 => HmacFunction::Sha256(HmacSha256::new_varkey(secret.as_bytes())?),
237        Algorithm::Sha512 => HmacFunction::Sha512(HmacSha512::new_varkey(secret.as_bytes())?),
238    })
239}
240
241#[doc(hidden)]
242fn generate_secret_ascii(length: u32, symbols: bool) -> String {
243    let byte_array: Vec<u8> = (0..length).map(|_| rand::random::<u8>()).collect();
244
245    let mut secret: String = String::from("");
246    for (_, value) in byte_array.iter().enumerate() {
247        // Need to decide to grab from the symbol/char set if configuration wants to add symbols to secret
248        if symbols {
249            secret.push(match value % 2 {
250                0 => CHAR_SET[((usize::from(value / 1)) * (CHAR_SET.len() - 1)) / 255],
251                1 => SYMBOL_SET[((usize::from(value / 1)) * (SYMBOL_SET.len() - 1)) / 255],
252                _ => unreachable!("Error: Reached the unreachable match arm of `u8` modulo 2"),
253            })
254        } else {
255            secret.push(CHAR_SET[((usize::from(value / 1)) * (CHAR_SET.len() - 1)) / 255])
256        }
257    }
258    secret
259}
260
261#[doc(hidden)]
262fn encode_uri_component(string: String) -> String {
263    byte_serialize(string.as_bytes()).collect()
264}
265
266#[doc(hidden)]
267fn generate_otpauth_url() {}
268
269#[cfg(test)]
270mod digest_tests {
271    use crate::digest;
272    use crate::Algorithm::Sha1;
273
274    #[test]
275    fn it_works() {
276        let test = digest("My secret".to_string(), 5000, Sha1);
277        match test {
278            Ok(result) => println!("Testing {:02x?}", result),
279            Err(_) => panic!("There was an error in the test"),
280        }
281    }
282}
283
284#[cfg(test)]
285mod generate_secret_tests {
286    use crate::{
287        generate_secret_ascii, generate_secret_without_symbols, generate_sized_secret, SYMBOL_SET,
288    };
289
290    #[test]
291    fn test_generate_secret_ascii_no_symbols() {
292        let secret = generate_secret_ascii(2000, false);
293        assert_eq!(secret.len(), 2000);
294    }
295
296    #[test]
297    fn test_generate_secret_ascii_symbols() {
298        let secret = generate_secret_ascii(2000, true);
299        assert_eq!(secret.len(), 2000);
300        assert_eq!(secret.contains("!"), true);
301    }
302
303    //    #[test]
304    //    fn test_generate_secret_defaults() {
305    //        assert_eq!(generate_secret().len(), 32);
306    //        assert_eq!(
307    //            generate_secret()
308    //                .chars()
309    //                .any(|c| match SYMBOL_SET.binary_search(&c) {
310    //                    Ok(_) => true,
311    //                    _ => false,
312    //                }),
313    //            true
314    //        )
315    //    }
316
317    #[test]
318    fn test_generate_secret_non_default_length() {
319        assert_eq!(generate_sized_secret(2000).len(), 2000);
320    }
321
322    #[test]
323    fn test_generate_secret_non_default_symbols() {
324        assert_eq!(
325            generate_secret_without_symbols()
326                .chars()
327                .any(|c| match SYMBOL_SET.binary_search(&c) {
328                    Ok(_) => true,
329                    _ => false,
330                }),
331            false
332        )
333    }
334}