1use crate::Algorithm;
2use crate::TotpUrlError;
3use crate::TOTP;
4
5#[cfg(feature = "serde_support")]
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Eq, PartialEq)]
10pub enum Rfc6238Error {
11 InvalidDigits(usize),
13 SecretTooSmall(usize),
15}
16
17impl std::error::Error for Rfc6238Error {}
18
19impl std::fmt::Display for Rfc6238Error {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Rfc6238Error::InvalidDigits(digits) => write!(
23 f,
24 "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed",
25 digits,
26 ),
27 Rfc6238Error::SecretTooSmall(bits) => write!(
28 f,
29 "The length of the shared secret MUST be at least 128 bits. {} bits is not enough",
30 bits,
31 ),
32 }
33 }
34}
35
36pub fn assert_digits(digits: &usize) -> Result<(), Rfc6238Error> {
39 if !(&6..=&8).contains(&digits) {
40 Err(Rfc6238Error::InvalidDigits(*digits))
41 } else {
42 Ok(())
43 }
44}
45
46pub fn assert_secret_length(secret: &[u8]) -> Result<(), Rfc6238Error> {
49 if secret.as_ref().len() < 16 {
50 Err(Rfc6238Error::SecretTooSmall(secret.as_ref().len() * 8))
51 } else {
52 Ok(())
53 }
54}
55
56#[derive(Debug, Clone)]
72#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
73pub struct Rfc6238 {
74 algorithm: Algorithm,
76 digits: usize,
78 skew: u8,
80 step: u64,
82 secret: Vec<u8>,
84 #[cfg(feature = "otpauth")]
85 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
86 issuer: Option<String>,
90 #[cfg(feature = "otpauth")]
91 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
92 account_name: String,
95}
96
97impl Rfc6238 {
98 #[cfg(feature = "otpauth")]
106 pub fn new(
107 digits: usize,
108 secret: Vec<u8>,
109 issuer: Option<String>,
110 account_name: String,
111 ) -> Result<Rfc6238, Rfc6238Error> {
112 assert_digits(&digits)?;
113 assert_secret_length(secret.as_ref())?;
114
115 Ok(Rfc6238 {
116 algorithm: Algorithm::SHA1,
117 digits,
118 skew: 1,
119 step: 30,
120 secret,
121 issuer,
122 account_name,
123 })
124 }
125 #[cfg(not(feature = "otpauth"))]
126 pub fn new(digits: usize, secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
127 assert_digits(&digits)?;
128 assert_secret_length(secret.as_ref())?;
129
130 Ok(Rfc6238 {
131 algorithm: Algorithm::SHA1,
132 digits,
133 skew: 1,
134 step: 30,
135 secret,
136 })
137 }
138
139 #[cfg(feature = "otpauth")]
148 pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
149 Rfc6238::new(6, secret, Some("".to_string()), "".to_string())
150 }
151
152 #[cfg(not(feature = "otpauth"))]
153 pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
154 Rfc6238::new(6, secret)
155 }
156
157 pub fn digits(&mut self, value: usize) -> Result<(), Rfc6238Error> {
159 assert_digits(&value)?;
160 self.digits = value;
161 Ok(())
162 }
163
164 #[cfg(feature = "otpauth")]
165 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
166 pub fn issuer(&mut self, value: String) {
168 self.issuer = Some(value);
169 }
170
171 #[cfg(feature = "otpauth")]
172 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
173 pub fn account_name(&mut self, value: String) {
175 self.account_name = value;
176 }
177}
178
179#[cfg(not(feature = "otpauth"))]
180impl TryFrom<Rfc6238> for TOTP {
181 type Error = TotpUrlError;
182
183 fn try_from(rfc: Rfc6238) -> Result<Self, Self::Error> {
185 TOTP::new(rfc.algorithm, rfc.digits, rfc.skew, rfc.step, rfc.secret)
186 }
187}
188
189#[cfg(feature = "otpauth")]
190impl TryFrom<Rfc6238> for TOTP {
191 type Error = TotpUrlError;
192
193 fn try_from(rfc: Rfc6238) -> Result<Self, Self::Error> {
195 TOTP::new(
196 rfc.algorithm,
197 rfc.digits,
198 rfc.skew,
199 rfc.step,
200 rfc.secret,
201 rfc.issuer,
202 rfc.account_name,
203 )
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 #[cfg(feature = "otpauth")]
210 use crate::TotpUrlError;
211
212 use super::{Rfc6238, TOTP};
213
214 #[cfg(not(feature = "otpauth"))]
215 use super::Rfc6238Error;
216
217 #[cfg(not(feature = "otpauth"))]
218 use crate::Secret;
219
220 const GOOD_SECRET: &str = "01234567890123456789";
221 #[cfg(feature = "otpauth")]
222 const ISSUER: Option<&str> = None;
223 #[cfg(feature = "otpauth")]
224 const ACCOUNT: &str = "valid-account";
225 #[cfg(feature = "otpauth")]
226 const INVALID_ACCOUNT: &str = ":invalid-account";
227
228 #[test]
229 #[cfg(not(feature = "otpauth"))]
230 fn new_rfc_digits() {
231 for x in 0..=20 {
232 let rfc = Rfc6238::new(x, GOOD_SECRET.into());
233 if !(6..=8).contains(&x) {
234 assert!(rfc.is_err());
235 assert!(matches!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
236 } else {
237 assert!(rfc.is_ok());
238 }
239 }
240 }
241
242 #[test]
243 #[cfg(not(feature = "otpauth"))]
244 fn new_rfc_secret() {
245 let mut secret = String::from("");
246 for _ in 0..=20 {
247 secret = format!("{}{}", secret, "0");
248 let rfc = Rfc6238::new(6, secret.as_bytes().to_vec());
249 let rfc_default = Rfc6238::with_defaults(secret.as_bytes().to_vec());
250 if secret.len() < 16 {
251 assert!(rfc.is_err());
252 assert!(matches!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall(_)));
253 assert!(rfc_default.is_err());
254 assert!(matches!(
255 rfc_default.unwrap_err(),
256 Rfc6238Error::SecretTooSmall(_)
257 ));
258 } else {
259 assert!(rfc.is_ok());
260 assert!(rfc_default.is_ok());
261 }
262 }
263 }
264
265 #[test]
266 #[cfg(not(feature = "otpauth"))]
267 fn rfc_to_totp_ok() {
268 let rfc = Rfc6238::new(8, GOOD_SECRET.into()).unwrap();
269 let totp = TOTP::try_from(rfc);
270 assert!(totp.is_ok());
271 let otp = totp.unwrap();
272 assert_eq!(&otp.secret, GOOD_SECRET.as_bytes());
273 assert_eq!(otp.algorithm, crate::Algorithm::SHA1);
274 assert_eq!(otp.digits, 8);
275 assert_eq!(otp.skew, 1);
276 assert_eq!(otp.step, 30)
277 }
278
279 #[test]
280 #[cfg(not(feature = "otpauth"))]
281 fn rfc_to_totp_ok_2() {
282 let rfc = Rfc6238::with_defaults(
283 Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string())
284 .to_bytes()
285 .unwrap(),
286 )
287 .unwrap();
288 let totp = TOTP::try_from(rfc);
289 assert!(totp.is_ok());
290 let otp = totp.unwrap();
291 assert_eq!(otp.algorithm, crate::Algorithm::SHA1);
292 assert_eq!(otp.digits, 6);
293 assert_eq!(otp.skew, 1);
294 assert_eq!(otp.step, 30)
295 }
296
297 #[test]
298 #[cfg(feature = "otpauth")]
299 fn rfc_to_totp_fail() {
300 let rfc = Rfc6238::new(
301 8,
302 GOOD_SECRET.as_bytes().to_vec(),
303 ISSUER.map(str::to_string),
304 INVALID_ACCOUNT.to_string(),
305 )
306 .unwrap();
307 let totp = TOTP::try_from(rfc);
308 assert!(totp.is_err());
309 assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)))
310 }
311
312 #[test]
313 #[cfg(feature = "otpauth")]
314 fn rfc_to_totp_ok() {
315 let rfc = Rfc6238::new(
316 8,
317 GOOD_SECRET.as_bytes().to_vec(),
318 ISSUER.map(str::to_string),
319 ACCOUNT.to_string(),
320 )
321 .unwrap();
322 let totp = TOTP::try_from(rfc);
323 assert!(totp.is_ok());
324 }
325
326 #[test]
327 #[cfg(feature = "otpauth")]
328 fn rfc_with_default_set_values() {
329 let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap();
330 let ok = rfc.digits(8);
331 assert!(ok.is_ok());
332 assert_eq!(rfc.account_name, "");
333 assert_eq!(rfc.issuer, Some("".to_string()));
334 rfc.issuer("Github".to_string());
335 rfc.account_name("constantoine".to_string());
336 assert_eq!(rfc.account_name, "constantoine");
337 assert_eq!(rfc.issuer, Some("Github".to_string()));
338 assert_eq!(rfc.digits, 8)
339 }
340
341 #[test]
342 #[cfg(not(feature = "otpauth"))]
343 fn rfc_with_default_set_values() {
344 let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap();
345 let fail = rfc.digits(4);
346 assert!(fail.is_err());
347 assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
348 assert_eq!(rfc.digits, 6);
349 let ok = rfc.digits(8);
350 assert!(ok.is_ok());
351 assert_eq!(rfc.digits, 8)
352 }
353
354 #[test]
355 #[cfg(not(feature = "otpauth"))]
356 fn digits_error() {
357 let error = crate::Rfc6238Error::InvalidDigits(9);
358 assert_eq!(
359 error.to_string(),
360 "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. 9 digits is not allowed".to_string()
361 )
362 }
363
364 #[test]
365 #[cfg(not(feature = "otpauth"))]
366 fn secret_length_error() {
367 let error = Rfc6238Error::SecretTooSmall(120);
368 assert_eq!(
369 error.to_string(),
370 "The length of the shared secret MUST be at least 128 bits. 120 bits is not enough"
371 .to_string()
372 )
373 }
374}