254 lines
8.1 KiB
Rust
254 lines
8.1 KiB
Rust
use crate::Algorithm;
|
|
use crate::LabeledTOTP;
|
|
use crate::TotpUrlError;
|
|
use crate::TOTP;
|
|
|
|
#[cfg(feature = "serde_support")]
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// 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
|
|
InvalidDigits(usize),
|
|
/// The length of the shared secret MUST be at least 128 bits
|
|
SecretTooSmall(usize),
|
|
}
|
|
|
|
impl std::error::Error for Rfc6238Error {}
|
|
|
|
impl std::fmt::Display for Rfc6238Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Rfc6238Error::InvalidDigits(digits) => write!(
|
|
f,
|
|
"Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed",
|
|
digits,
|
|
),
|
|
Rfc6238Error::SecretTooSmall(bits) => write!(
|
|
f,
|
|
"The length of the shared secret MUST be at least 128 bits. {} bits is not enough",
|
|
bits,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn assert_digits(digits: &usize) -> Result<(), Rfc6238Error> {
|
|
if !(&6..=&8).contains(&digits) {
|
|
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))
|
|
} 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".as_bytes().to_vec()
|
|
/// ).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 {
|
|
/// 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: Vec<u8>,
|
|
}
|
|
|
|
impl Rfc6238 {
|
|
/// 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)
|
|
pub fn new(digits: usize, secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
|
|
assert_digits(&digits)?;
|
|
assert_secret_length(secret.as_ref())?;
|
|
|
|
Ok(Rfc6238 {
|
|
algorithm: Algorithm::SHA1,
|
|
digits,
|
|
skew: 1,
|
|
step: 30,
|
|
secret,
|
|
})
|
|
}
|
|
|
|
/// 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)
|
|
pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
|
|
Rfc6238::new(6, secret)
|
|
}
|
|
|
|
/// Set the `digits`
|
|
pub fn digits(&mut self, value: usize) -> Result<(), Rfc6238Error> {
|
|
assert_digits(&value)?;
|
|
self.digits = value;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Rfc6238> for TOTP {
|
|
type Error = TotpUrlError;
|
|
|
|
/// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config
|
|
fn try_from(rfc: Rfc6238) -> Result<Self, Self::Error> {
|
|
TOTP::new(rfc.algorithm, rfc.digits, rfc.skew, rfc.step, rfc.secret)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Rfc6238> for LabeledTOTP {
|
|
type Error = TotpUrlError;
|
|
|
|
/// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config
|
|
fn try_from(rfc: Rfc6238) -> Result<Self, Self::Error> {
|
|
TOTP::new(
|
|
rfc.algorithm,
|
|
rfc.digits,
|
|
rfc.skew,
|
|
rfc.step,
|
|
rfc.secret,
|
|
).map(LabeledTOTP::from)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{Rfc6238, TOTP};
|
|
|
|
use super::Rfc6238Error;
|
|
|
|
use crate::Secret;
|
|
|
|
const GOOD_SECRET: &str = "01234567890123456789";
|
|
|
|
#[test]
|
|
fn new_rfc_digits() {
|
|
for x in 0..=20 {
|
|
let rfc = Rfc6238::new(x, GOOD_SECRET.into());
|
|
if !(6..=8).contains(&x) {
|
|
assert!(rfc.is_err());
|
|
assert!(matches!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
|
|
} else {
|
|
assert!(rfc.is_ok());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn new_rfc_secret() {
|
|
let mut secret = String::from("");
|
|
for _ in 0..=20 {
|
|
secret = format!("{}{}", secret, "0");
|
|
let rfc = Rfc6238::new(6, secret.as_bytes().to_vec());
|
|
let rfc_default = Rfc6238::with_defaults(secret.as_bytes().to_vec());
|
|
if secret.len() < 16 {
|
|
assert!(rfc.is_err());
|
|
assert!(matches!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall(_)));
|
|
assert!(rfc_default.is_err());
|
|
assert!(matches!(
|
|
rfc_default.unwrap_err(),
|
|
Rfc6238Error::SecretTooSmall(_)
|
|
));
|
|
} else {
|
|
assert!(rfc.is_ok());
|
|
assert!(rfc_default.is_ok());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rfc_to_totp_ok() {
|
|
let rfc = Rfc6238::new(8, GOOD_SECRET.into()).unwrap();
|
|
let totp = TOTP::try_from(rfc);
|
|
assert!(totp.is_ok());
|
|
let otp = totp.unwrap();
|
|
assert_eq!(&otp.secret, GOOD_SECRET.as_bytes());
|
|
assert_eq!(otp.algorithm, crate::Algorithm::SHA1);
|
|
assert_eq!(otp.digits, 8);
|
|
assert_eq!(otp.skew, 1);
|
|
assert_eq!(otp.step, 30)
|
|
}
|
|
|
|
#[test]
|
|
fn rfc_to_totp_ok_2() {
|
|
let rfc = Rfc6238::with_defaults(
|
|
Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string())
|
|
.to_bytes()
|
|
.unwrap(),
|
|
)
|
|
.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]
|
|
fn rfc_with_default_set_values() {
|
|
let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap();
|
|
let fail = rfc.digits(4);
|
|
assert!(fail.is_err());
|
|
assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
|
|
assert_eq!(rfc.digits, 6);
|
|
let ok = rfc.digits(8);
|
|
assert!(ok.is_ok());
|
|
assert_eq!(rfc.digits, 8)
|
|
}
|
|
|
|
#[test]
|
|
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]
|
|
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()
|
|
)
|
|
}
|
|
}
|