2022-08-06 11:31:11 -04:00
use crate ::Algorithm ;
use crate ::TotpUrlError ;
use crate ::TOTP ;
2022-08-08 08:17:23 -04:00
#[ cfg(feature = " serde_support " ) ]
use serde ::{ Deserialize , Serialize } ;
2022-08-06 11:31:11 -04:00
/// Data is not compliant to [rfc-6238](https://tools.ietf.org/html/rfc6238)
#[ derive(Debug, Eq, PartialEq) ]
pub enum Rfc6238Error {
/// Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code
2022-08-08 13:52:54 -04:00
InvalidDigits ( usize ) ,
2022-08-06 11:31:11 -04:00
/// The length of the shared secret MUST be at least 128 bits
2022-08-08 13:52:54 -04:00
SecretTooSmall ( usize ) ,
2022-08-06 11:31:11 -04:00
}
impl std ::fmt ::Display for Rfc6238Error {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
match self {
2022-08-08 13:52:54 -04:00
Rfc6238Error ::InvalidDigits ( digits ) = > write! (
2022-08-06 11:31:11 -04:00
f ,
2022-08-08 13:52:54 -04:00
" Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed " ,
digits ,
2022-08-06 11:31:11 -04:00
) ,
2022-08-08 13:52:54 -04:00
Rfc6238Error ::SecretTooSmall ( bits ) = > write! (
2022-08-06 11:31:11 -04:00
f ,
2022-08-08 13:52:54 -04:00
" The length of the shared secret MUST be at least 128 bits. {} bits is not enough " ,
bits ,
2022-08-06 11:31:11 -04:00
) ,
}
}
}
2022-08-08 11:30:27 -04:00
pub fn assert_digits ( digits : & usize ) -> Result < ( ) , Rfc6238Error > {
2022-08-08 08:10:39 -04:00
if ! ( & 6 ..= & 8 ) . contains ( & digits ) {
2022-08-08 13:52:54 -04:00
Err ( Rfc6238Error ::InvalidDigits ( * digits ) )
} else {
Ok ( ( ) )
}
}
pub fn assert_secret_length ( secret : & [ u8 ] ) -> Result < ( ) , Rfc6238Error > {
if secret . as_ref ( ) . len ( ) < 16 {
Err ( Rfc6238Error ::SecretTooSmall ( secret . as_ref ( ) . len ( ) * 8 ) )
2022-08-06 11:31:11 -04:00
} else {
Ok ( ( ) )
}
}
/// [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options to create a [TOTP](struct.TOTP.html)
///
/// # Example
/// ```
/// use totp_rs::{Rfc6238, TOTP};
///
/// let mut rfc = Rfc6238::with_defaults(
/// "totp-sercret-123"
/// ).unwrap();
///
/// // optional, set digits, issuer, account_name
/// rfc.digits(8).unwrap();
///
/// let totp = TOTP::from_rfc6238(rfc).unwrap();
/// ```
#[ derive(Debug, Clone) ]
#[ cfg_attr(feature = " serde_support " , derive(Serialize, Deserialize)) ]
pub struct Rfc6238 < T = Vec < u8 > > {
/// SHA-1
algorithm : Algorithm ,
/// The number of digits composing the auth code. Per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-5.3), this can oscilate between 6 and 8 digits
digits : usize ,
/// The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 1.
skew : u8 ,
/// The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 30 seconds
step : u64 ,
/// As per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-4) the secret should come from a strong source, most likely a CSPRNG. It should be at least 128 bits, but 160 are recommended
secret : T ,
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
/// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:`
/// For example, the name of your service/website.
/// Not mandatory, but strongly recommended!
issuer : Option < String > ,
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
/// The "constantoine@github.com" part of "Github:constantoine@github.com". Must not contain a colon `:`
/// For example, the name of your user's account.
account_name : String ,
}
impl < T : AsRef < [ u8 ] > > Rfc6238 < T > {
/// Create an [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options that can be turned into a [TOTP](struct.TOTP.html)
///
/// # Errors
///
/// will return a [Rfc6238Error](enum.Rfc6238Error.html) when
/// - `digits` is lower than 6 or higher than 8
/// - `secret` is smaller than 128 bits (16 characters)
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
pub fn new (
digits : usize ,
secret : T ,
issuer : Option < String > ,
account_name : String ,
) -> Result < Rfc6238 < T > , Rfc6238Error > {
assert_digits ( & digits ) ? ;
2022-08-08 13:52:54 -04:00
assert_secret_length ( secret . as_ref ( ) ) ? ;
2022-10-05 05:55:37 -04:00
2022-08-08 13:52:54 -04:00
Ok ( Rfc6238 {
algorithm : Algorithm ::SHA1 ,
digits ,
skew : 1 ,
step : 30 ,
secret ,
issuer ,
account_name ,
} )
2022-08-06 11:31:11 -04:00
}
2022-08-09 05:05:36 -04:00
#[ cfg(not(feature = " otpauth " )) ]
2022-10-05 05:55:37 -04:00
pub fn new ( digits : usize , secret : T ) -> Result < Rfc6238 < T > , Rfc6238Error > {
2022-08-09 05:05:36 -04:00
assert_digits ( & digits ) ? ;
assert_secret_length ( secret . as_ref ( ) ) ? ;
2022-10-05 05:55:37 -04:00
2022-08-09 05:05:36 -04:00
Ok ( Rfc6238 {
algorithm : Algorithm ::SHA1 ,
digits ,
skew : 1 ,
step : 30 ,
secret ,
} )
}
2022-08-06 11:31:11 -04:00
/// Create an [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options that can be turned into a [TOTP](struct.TOTP.html),
/// with a default value of 6 for `digits`, None `issuer` and an empty account
///
/// # Errors
///
/// will return a [Rfc6238Error](enum.Rfc6238Error.html) when
/// - `digits` is lower than 6 or higher than 8
/// - `secret` is smaller than 128 bits (16 characters)
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
pub fn with_defaults ( secret : T ) -> Result < Rfc6238 < T > , Rfc6238Error > {
2022-08-09 05:05:36 -04:00
Rfc6238 ::new ( 6 , secret , Some ( " " . to_string ( ) ) , " " . to_string ( ) )
}
#[ cfg(not(feature = " otpauth " )) ]
pub fn with_defaults ( secret : T ) -> Result < Rfc6238 < T > , Rfc6238Error > {
Rfc6238 ::new ( 6 , secret )
2022-08-06 11:31:11 -04:00
}
/// Set the `digits`
2022-10-05 05:55:37 -04:00
pub fn digits ( & mut self , value : usize ) -> Result < ( ) , Rfc6238Error > {
2022-08-06 11:31:11 -04:00
assert_digits ( & value ) ? ;
self . digits = value ;
Ok ( ( ) )
}
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
/// Set the `issuer`
pub fn issuer ( & mut self , value : String ) {
self . issuer = Some ( value ) ;
}
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
/// Set the `account_name`
2022-08-06 11:31:11 -04:00
pub fn account_name ( & mut self , value : String ) {
self . account_name = value ;
}
}
2022-08-09 05:05:36 -04:00
#[ cfg(not(feature = " otpauth " )) ]
impl < T : AsRef < [ u8 ] > > TryFrom < Rfc6238 < T > > for TOTP < T > {
type Error = TotpUrlError ;
/// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config
fn try_from ( rfc : Rfc6238 < T > ) -> Result < Self , Self ::Error > {
2022-10-05 05:55:37 -04:00
TOTP ::new ( rfc . algorithm , rfc . digits , rfc . skew , rfc . step , rfc . secret )
2022-08-09 05:05:36 -04:00
}
}
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
impl < T : AsRef < [ u8 ] > > TryFrom < Rfc6238 < T > > for TOTP < T > {
type Error = TotpUrlError ;
/// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config
fn try_from ( rfc : Rfc6238 < T > ) -> Result < Self , Self ::Error > {
TOTP ::new (
rfc . algorithm ,
rfc . digits ,
rfc . skew ,
rfc . step ,
rfc . secret ,
rfc . issuer ,
2022-10-05 05:55:37 -04:00
rfc . account_name ,
2022-08-06 11:31:11 -04:00
)
}
}
#[ cfg(test) ]
mod tests {
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
use crate ::TotpUrlError ;
2022-08-09 05:05:36 -04:00
use super ::{ Rfc6238 , TOTP } ;
#[ cfg(not(feature = " otpauth " )) ]
use super ::Rfc6238Error ;
#[ cfg(not(feature = " otpauth " )) ]
use crate ::Secret ;
2022-08-06 11:31:11 -04:00
const GOOD_SECRET : & str = " 01234567890123456789 " ;
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
const ISSUER : Option < & str > = None ;
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
const ACCOUNT : & str = " valid-account " ;
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
const INVALID_ACCOUNT : & str = " :invalid-account " ;
#[ test ]
2022-08-09 05:05:36 -04:00
#[ cfg(not(feature = " otpauth " )) ]
2022-08-06 11:31:11 -04:00
fn new_rfc_digits ( ) {
for x in 0 ..= 20 {
2022-10-05 05:55:37 -04:00
let rfc = Rfc6238 ::new ( x , GOOD_SECRET . to_string ( ) ) ;
2022-08-08 13:52:54 -04:00
if ! ( 6 ..= 8 ) . contains ( & x ) {
2022-08-06 11:31:11 -04:00
assert! ( rfc . is_err ( ) ) ;
2022-08-08 13:52:54 -04:00
assert! ( matches! ( rfc . unwrap_err ( ) , Rfc6238Error ::InvalidDigits ( _ ) ) ) ;
2022-08-06 11:31:11 -04:00
} else {
assert! ( rfc . is_ok ( ) ) ;
}
}
}
#[ test ]
2022-08-09 05:05:36 -04:00
#[ cfg(not(feature = " otpauth " )) ]
2022-08-06 11:31:11 -04:00
fn new_rfc_secret ( ) {
let mut secret = String ::from ( " " ) ;
for _ in 0 ..= 20 {
secret = format! ( " {} {} " , secret , " 0 " ) ;
2022-10-05 05:55:37 -04:00
let rfc = Rfc6238 ::new ( 6 , secret . clone ( ) ) ;
2022-08-06 11:31:11 -04:00
let rfc_default = Rfc6238 ::with_defaults ( secret . clone ( ) ) ;
if secret . len ( ) < 16 {
assert! ( rfc . is_err ( ) ) ;
2022-08-08 13:52:54 -04:00
assert! ( matches! ( rfc . unwrap_err ( ) , Rfc6238Error ::SecretTooSmall ( _ ) ) ) ;
2022-08-06 11:31:11 -04:00
assert! ( rfc_default . is_err ( ) ) ;
2022-10-05 05:55:37 -04:00
assert! ( matches! (
rfc_default . unwrap_err ( ) ,
Rfc6238Error ::SecretTooSmall ( _ )
) ) ;
2022-08-06 11:31:11 -04:00
} else {
assert! ( rfc . is_ok ( ) ) ;
assert! ( rfc_default . is_ok ( ) ) ;
}
}
}
#[ test ]
2022-08-09 05:05:36 -04:00
#[ cfg(not(feature = " otpauth " )) ]
2022-08-06 11:31:11 -04:00
fn rfc_to_totp_ok ( ) {
2022-10-05 05:55:37 -04:00
let rfc = Rfc6238 ::new ( 8 , GOOD_SECRET . to_string ( ) ) . unwrap ( ) ;
2022-08-06 11:31:11 -04:00
let totp = TOTP ::try_from ( rfc ) ;
assert! ( totp . is_ok ( ) ) ;
let otp = totp . unwrap ( ) ;
assert_eq! ( & otp . secret , GOOD_SECRET ) ;
assert_eq! ( otp . algorithm , crate ::Algorithm ::SHA1 ) ;
assert_eq! ( otp . digits , 8 ) ;
assert_eq! ( otp . skew , 1 ) ;
assert_eq! ( otp . step , 30 )
}
#[ test ]
2022-08-09 05:05:36 -04:00
#[ cfg(not(feature = " otpauth " )) ]
fn rfc_to_totp_ok_2 ( ) {
let rfc = Rfc6238 ::with_defaults (
2022-10-05 05:55:37 -04:00
Secret ::Encoded ( " KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ " . to_string ( ) )
. to_bytes ( )
. unwrap ( ) ,
2022-08-09 05:05:36 -04:00
)
. unwrap ( ) ;
let totp = TOTP ::try_from ( rfc ) ;
assert! ( totp . is_ok ( ) ) ;
let otp = totp . unwrap ( ) ;
assert_eq! ( otp . algorithm , crate ::Algorithm ::SHA1 ) ;
assert_eq! ( otp . digits , 6 ) ;
assert_eq! ( otp . skew , 1 ) ;
assert_eq! ( otp . step , 30 )
}
#[ test ]
#[ cfg(feature = " otpauth " ) ]
2022-08-06 11:31:11 -04:00
fn rfc_to_totp_fail ( ) {
let rfc = Rfc6238 ::new (
8 ,
GOOD_SECRET . to_string ( ) ,
ISSUER . map ( str ::to_string ) ,
INVALID_ACCOUNT . to_string ( ) ,
)
. unwrap ( ) ;
let totp = TOTP ::try_from ( rfc ) ;
assert! ( totp . is_err ( ) ) ;
2022-08-08 13:52:54 -04:00
assert! ( matches! ( totp . unwrap_err ( ) , TotpUrlError ::AccountName ( _ ) ) )
2022-08-06 11:31:11 -04:00
}
#[ test ]
2022-08-09 05:05:36 -04:00
#[ cfg(feature = " otpauth " ) ]
fn rfc_to_totp_ok ( ) {
let rfc = Rfc6238 ::new (
8 ,
GOOD_SECRET . to_string ( ) ,
ISSUER . map ( str ::to_string ) ,
ACCOUNT . to_string ( ) ,
)
. unwrap ( ) ;
let totp = TOTP ::try_from ( rfc ) ;
2022-10-05 05:45:21 -04:00
assert! ( totp . is_ok ( ) ) ;
2022-08-09 05:05:36 -04:00
}
2022-10-05 11:32:31 -04:00
#[ test ]
#[ cfg(feature = " otpauth " ) ]
fn rfc_with_default_set_values ( ) {
let mut rfc = Rfc6238 ::with_defaults ( GOOD_SECRET . to_string ( ) ) . unwrap ( ) ;
let ok = rfc . digits ( 8 ) ;
assert! ( ok . is_ok ( ) ) ;
assert_eq! ( rfc . account_name , " " ) ;
assert_eq! ( rfc . issuer , Some ( " " . to_string ( ) ) ) ;
rfc . issuer ( " Github " . to_string ( ) ) ;
rfc . account_name ( " constantoine " . to_string ( ) ) ;
assert_eq! ( rfc . account_name , " constantoine " ) ;
assert_eq! ( rfc . issuer , Some ( " Github " . to_string ( ) ) ) ;
assert_eq! ( rfc . digits , 8 )
}
2022-08-09 05:05:36 -04:00
#[ test ]
#[ cfg(not(feature = " otpauth " )) ]
2022-08-06 11:31:11 -04:00
fn rfc_with_default_set_values ( ) {
let mut rfc = Rfc6238 ::with_defaults ( GOOD_SECRET . to_string ( ) ) . unwrap ( ) ;
let fail = rfc . digits ( 4 ) ;
assert! ( fail . is_err ( ) ) ;
2022-08-08 13:52:54 -04:00
assert! ( matches! ( fail . unwrap_err ( ) , Rfc6238Error ::InvalidDigits ( _ ) ) ) ;
2022-08-06 11:31:11 -04:00
assert_eq! ( rfc . digits , 6 ) ;
let ok = rfc . digits ( 8 ) ;
assert! ( ok . is_ok ( ) ) ;
assert_eq! ( rfc . digits , 8 )
}
2022-10-05 11:32:31 -04:00
#[ test ]
#[ cfg(not(feature = " otpauth " )) ]
fn digits_error ( ) {
let error = crate ::Rfc6238Error ::InvalidDigits ( 9 ) ;
assert_eq! (
error . to_string ( ) ,
" Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. 9 digits is not allowed " . to_string ( )
)
}
#[ test ]
#[ cfg(not(feature = " otpauth " )) ]
fn secret_length_error ( ) {
let error = Rfc6238Error ::SecretTooSmall ( 120 ) ;
assert_eq! (
error . to_string ( ) ,
" The length of the shared secret MUST be at least 128 bits. 120 bits is not enough " . to_string ( )
)
}
2022-08-06 11:31:11 -04:00
}