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