yubi_opt/
lib.rs

1//! # YubiOTP
2//!
3//! This crate allows you to verify Yubico OTPs.
4//! To get details about what a Yubico OTP is, you can read about it here: <https://developers.yubico.com/OTP/>
5//!
6//! In order to use this crate you should first check the OTP of the user with the `is_valid_otp` function.
7//! If the OTP has the correct format, you can send the OTP to Yubico for verification. To do that, you
8//! will have to create a `YubicoClient` with your credentials.
9//! You can then call the `verify` function on it to send the request.
10//! Check if the result has the state `Ok`, if it's any other state the OTP is invalid!
11//!
12//! In case the state was `Ok` you will probably also want to check the public id of the key to compare it against the one you have on record.
13//! To get the public id of any OTP you can call the `get_public_id` function.
14
15mod response_state;
16pub mod yubico_client;
17
18const OTP_MIN_LENGTH: usize = 32;
19const OTP_MAX_LENGTH: usize = 48;
20
21/// Validates if a give OTP if in a valid format.
22///
23/// # Arguments
24///
25/// * `otp`: The OTP which format should be checked
26///
27/// returns: bool
28///
29/// # Examples
30///
31/// ```
32/// assert!(yubi_opt::is_valid_otp("cccjgjgkhcbbcvchfkfhiiuunbtnvgihdfiktncvlhck"));
33/// ```
34pub fn is_valid_otp(otp: &str) -> bool {
35    if (otp.len() < OTP_MIN_LENGTH) || (otp.len() > OTP_MAX_LENGTH) {
36        return false;
37    }
38
39    return otp
40        .chars()
41        .map(|c| c as u32)
42        .all(|c| (0x20..=0x7E).contains(&c));
43}
44
45/// Will return the public id of an OTP.
46///
47/// # Arguments
48///
49/// * `otp`: OTP from which the public id should be extracted
50///
51/// returns: Result<&str, &str>
52///
53/// # Examples
54///
55/// ```
56/// let public_id = yubi_opt::get_public_id("cccjgjgkhcbbcvchfkfhiiuunbtnvgihdfiktncvlhck").unwrap();
57/// assert_eq!(public_id, "cccjgjgkhcbb")
58/// ```
59pub fn get_public_id(otp: &str) -> Result<&str, &str> {
60    if !is_valid_otp(otp) {
61        return Err("OTP is invalid");
62    }
63
64    //Last 32 chars will always be the unique passcode
65    Ok(otp.split_at(otp.len() - 32).0)
66}
67
68#[cfg(test)]
69mod tests {
70    use crate::response_state::State;
71    use crate::yubico_client::YubicoClient;
72
73    use super::*;
74
75    #[test]
76    fn test_opt_validator() {
77        let valid_otp = is_valid_otp("cccjgjgkhcbbcvchfkfhiiuunbtnvgihdfiktncvlhck");
78        let another_valid_otp = is_valid_otp("cccjgjgkhcbbgefdkbbditfjrlniggevfhenublfnrev");
79        let invalid_otp_char = is_valid_otp("cccjgjgkhcbbcvchfkツhiiuunbtnvgihdfiktncvlhck");
80        let invalid_otp_length = is_valid_otp("gefdkbbditfjrlniggevfh");
81
82        assert!(valid_otp);
83        assert!(another_valid_otp);
84        assert_eq!(invalid_otp_char, false);
85        assert_eq!(invalid_otp_length, false);
86    }
87
88    #[test]
89    fn test_get_public_id() {
90        let valid_otp = get_public_id("cccjgjgkhcbbcvchfkfhiiuunbtnvgihdfiktncvlhck");
91        let another_valid_otp = get_public_id("cccjgjgkhcbbgefdkbbditfjrlniggevfhenublfnrev");
92        let invalid_otp = get_public_id("gefdkbbditfjrlniggevfh");
93
94        assert_eq!(valid_otp.ok().unwrap(), "cccjgjgkhcbb");
95        assert_eq!(another_valid_otp.ok().unwrap(), "cccjgjgkhcbb");
96        assert!(invalid_otp.err().is_some());
97    }
98
99    //TODO: Make a more decent test for this, currently its using the client_id and api_key from the
100    //      Test Vectors site: https://developers.yubico.com/OTP/Specifications/Test_vectors.html
101    //      However the OTP will always return `ReplayedOtp` and not `Ok` which is unfortunate.
102    //      Should be enough for the start tho to see if the http requests are successful.
103    #[test]
104    fn test_yubico_client() {
105        //Hit: Do NOT use the credentials here, these are example credentials from the documentation!
106        //Instead get your own api_key and client_id from here: https://upgrade.yubico.com/getapikey/
107        let client = YubicoClient::new(1, None);
108
109        let x = client
110            .verify("vvungrrdhvtklknvrtvuvbbkeidikkvgglrvdgrfcdft")
111            .unwrap();
112
113        assert_eq!(x.state, State::ReplayedOtp)
114    }
115}