t_rust_less_lib/otp/
mod.rs1use 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}