t_rust_less_lib/otp/
mod.rs

1use std::fmt;
2use url::{form_urlencoded, Url};
3
4mod error;
5mod hotp;
6mod totp;
7
8#[cfg(test)]
9mod tests;
10
11pub use self::error::*;
12use crate::otp::hotp::HOTPGenerator;
13use crate::otp::totp::TOTPGenerator;
14use std::str::FromStr;
15use zeroize::Zeroize;
16
17const OTP_URL_SCHEME: &str = "otpauth";
18
19pub enum OTPType {
20  Totp { period: u32 },
21  Hotp { counter: u64 },
22}
23
24impl fmt::Display for OTPType {
25  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26    match self {
27      OTPType::Totp { .. } => write!(f, "totp")?,
28      OTPType::Hotp { .. } => write!(f, "hotp")?,
29    }
30    Ok(())
31  }
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum OTPAlgorithm {
36  SHA1,
37  SHA256,
38  SHA512,
39}
40
41impl fmt::Display for OTPAlgorithm {
42  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43    match self {
44      OTPAlgorithm::SHA1 => write!(f, "SHA1")?,
45      OTPAlgorithm::SHA256 => write!(f, "SHA256")?,
46      OTPAlgorithm::SHA512 => write!(f, "SHA512")?,
47    }
48    Ok(())
49  }
50}
51
52#[derive(Zeroize)]
53#[zeroize(drop)]
54pub struct OTPSecret(Vec<u8>);
55
56impl fmt::Display for OTPSecret {
57  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58    write!(f, "{}", data_encoding::BASE32_NOPAD.encode(&self.0))
59  }
60}
61
62impl FromStr for OTPSecret {
63  type Err = OTPError;
64
65  fn from_str(s: &str) -> OTPResult<Self> {
66    match data_encoding::BASE32_NOPAD.decode(s.to_uppercase().as_bytes()) {
67      Ok(bytes) => Ok(OTPSecret(bytes)),
68      Err(_) => Err(OTPError::InvalidSecret),
69    }
70  }
71}
72
73pub struct OTPAuthUrl {
74  pub otp_type: OTPType,
75  pub algorithm: OTPAlgorithm,
76  pub digits: u8,
77  pub account_name: String,
78  pub issuer: Option<String>,
79  pub secret: OTPSecret,
80}
81
82impl OTPAuthUrl {
83  pub fn parse<S: AsRef<str>>(url_str: S) -> OTPResult<OTPAuthUrl> {
84    let url = Url::parse(url_str.as_ref())?;
85    if url.scheme() != OTP_URL_SCHEME {
86      return Err(OTPError::InvalidScheme);
87    }
88    let otp_type = match url.host_str() {
89      Some("totp") => {
90        let period = Self::find_parameter(&url, "period")?.unwrap_or(30);
91        OTPType::Totp { period }
92      }
93      Some("hotp") => {
94        let counter = Self::find_required_parameter(&url, "counter")?;
95        OTPType::Hotp { counter }
96      }
97      _ => return Err(OTPError::InvalidType),
98    };
99    let mut issuer = Self::find_parameter::<String>(&url, "issuer")?;
100    let mut account_name = String::new();
101    if url.path().is_empty() {
102      return Err(OTPError::MissingParameter("accountname".to_string()));
103    } else {
104      let mut parts = url.path()[1..].split(':');
105      if let Some(issuer_or_account) = parts.next() {
106        account_name = issuer_or_account.to_string();
107      }
108      if let Some(account) = parts.next() {
109        issuer = Some(account_name);
110        account_name = account.to_string();
111      }
112    }
113    let algorithm = match Self::find_parameter::<String>(&url, "algorithm")?.as_deref() {
114      Some("SHA1") | None => OTPAlgorithm::SHA1,
115      Some("SHA256") => OTPAlgorithm::SHA256,
116      Some("SHA512") => OTPAlgorithm::SHA512,
117      Some(_) => return Err(OTPError::InvalidAlgorithm),
118    };
119    let digits = Self::find_parameter(&url, "digits")?.unwrap_or(6);
120    let secret = Self::find_required_parameter(&url, "secret")?;
121
122    Ok(OTPAuthUrl {
123      otp_type,
124      algorithm,
125      digits,
126      account_name,
127      issuer,
128      secret,
129    })
130  }
131
132  pub fn to_url(&self) -> String {
133    let mut result = format!("{}://{}/", OTP_URL_SCHEME, self.otp_type);
134
135    if let Some(issuer) = &self.issuer {
136      result.extend(form_urlencoded::byte_serialize(issuer.as_bytes()));
137      result += ":"
138    }
139    result.extend(form_urlencoded::byte_serialize(self.account_name.as_bytes()));
140    result += "?secret=";
141    result += &self.secret.to_string();
142
143    match self.otp_type {
144      OTPType::Totp { period } if period != 30 => {
145        result += "&period=";
146        result += &period.to_string();
147      }
148      OTPType::Totp { .. } => (),
149      OTPType::Hotp { counter } => {
150        result += "&counter=";
151        result += &counter.to_string();
152      }
153    }
154    if self.digits != 6 {
155      result += "&digits=";
156      result += &self.digits.to_string();
157    }
158    if let Some(issuer) = &self.issuer {
159      result += "&issuer=";
160      result.extend(form_urlencoded::byte_serialize(issuer.as_bytes()));
161    }
162    if self.algorithm != OTPAlgorithm::SHA1 {
163      result += "&algorithm=";
164      result += &self.algorithm.to_string();
165    }
166
167    result
168  }
169
170  pub fn generate(&self, timestamp_or_counter: u64) -> (String, u64) {
171    match self.otp_type {
172      OTPType::Totp { period } => TOTPGenerator {
173        algorithm: self.algorithm,
174        digits: self.digits,
175        period,
176        secret: &self.secret.0,
177      }
178      .generate(timestamp_or_counter),
179      OTPType::Hotp { .. } => HOTPGenerator {
180        algorithm: self.algorithm,
181        digits: self.digits,
182        counter: timestamp_or_counter,
183        secret: &self.secret.0,
184      }
185      .generate(),
186    }
187  }
188
189  fn find_parameter<T: FromStr>(url: &Url, name: &str) -> OTPResult<Option<T>> {
190    match url.query_pairs().find(|(key, _)| key == name) {
191      Some((_, value)) => {
192        let t = value
193          .parse::<T>()
194          .map_err(|_| OTPError::MissingParameter(name.to_string()))?;
195        Ok(Some(t))
196      }
197      None => Ok(None),
198    }
199  }
200
201  fn find_required_parameter<T: FromStr>(url: &Url, name: &str) -> OTPResult<T> {
202    Self::find_parameter(url, name)?.ok_or_else(|| OTPError::MissingParameter(name.to_string()))
203  }
204}