totp-rs/src/rfc.rs

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()
)
}
}