totp-rs 5.7.2

RFC-compliant TOTP implementation with ease of use as a goal and additionnal QoL features.
Documentation
#[cfg(feature = "otpauth")]
use url::ParseError;

use crate::Rfc6238Error;

/// Errors returned mostly upon decoding URL.
#[derive(Debug, Eq, PartialEq)]
pub enum TotpUrlError {
    /// Couldn't decode URL.
    #[cfg(feature = "otpauth")]
    #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
    Url(ParseError),
    /// Invalid scheme.
    Scheme(String),
    /// Invalid host.
    Host(String),
    /// Wrong base32 input.
    Secret(String),
    /// Invalid secret size. (Too short?)
    SecretSize(usize),
    /// Unknown algorithm.
    Algorithm(String),
    /// Characters should only be digits.
    Digits(String),
    /// Digits should be between 6 and 8.
    DigitsNumber(usize),
    /// Couldn't decode step into a number.
    Step(String),
    /// Issuer contains invalid character `:`.
    Issuer(String),
    /// Couldn't decode issuer.
    IssuerDecoding(String),
    /// Issuers should be the same.
    IssuerMistmatch(String, String),
    /// Account name contains invalid character `:` or couldn't be decoded.
    AccountName(String),
    /// Couldn't parse account name.
    AccountNameDecoding(String),
}

impl std::error::Error for TotpUrlError {}

impl std::fmt::Display for TotpUrlError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TotpUrlError::AccountName(name) => write!(
                f,
                "Account Name can't contain a colon. \"{}\" contains a colon",
                name
            ),
            TotpUrlError::AccountNameDecoding(name) => write!(
                f,
                "Couldn't URL decode \"{}\"",
                name
            ),
            TotpUrlError::Algorithm(algo) => write!(
                f,
                "Algorithm can only be SHA1, SHA256 or SHA512, not \"{}\"",
                algo
            ),
            TotpUrlError::Digits(digits) => write!(
                f,
                "Could not parse \"{}\" as a number.",
                digits,
            ),
            TotpUrlError::DigitsNumber(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,
            ),
            TotpUrlError::Host(host) => write!(
                f,
                "Host should be totp, not \"{}\"",
                host
            ),
            TotpUrlError::Issuer(issuer) => write!(
                f,
                "Issuer can't contain a colon. \"{}\" contains a colon",
                issuer
            ),
            TotpUrlError::IssuerDecoding(issuer) => write!(
                f,
                "Couldn't URL decode \"{}\"",
                issuer
            ),
            TotpUrlError::IssuerMistmatch(path_issuer, issuer) => write!(
                f,
                "An issuer \"{}\" could be retrieved from the path, but a different issuer \"{}\" was found in the issuer URL parameter",
                path_issuer,
                issuer,
            ),
            TotpUrlError::Scheme(scheme) => write!(
                f,
                "Scheme should be otpauth, not \"{}\"",
                scheme
            ),
            TotpUrlError::Secret(secret) => write!(
                f,
                "Secret \"{}\" is not a valid non-padded base32 string",
                secret,
            ),
            TotpUrlError::SecretSize(bits) => write!(
                f,
                "The length of the shared secret MUST be at least 128 bits. {} bits is not enough",
                bits,
            ),
            TotpUrlError::Step(step) => write!(
                f,
                "Could not parse \"{}\" as a number.",
                step,
            ),
            #[cfg(feature = "otpauth")]
            TotpUrlError::Url(e) => write!(
                f,
                "Error parsing URL: {}",
                e
            )
        }
    }
}

impl From<Rfc6238Error> for TotpUrlError {
    fn from(e: Rfc6238Error) -> Self {
        match e {
            Rfc6238Error::InvalidDigits(digits) => TotpUrlError::DigitsNumber(digits),
            Rfc6238Error::SecretTooSmall(bits) => TotpUrlError::SecretSize(bits),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::TotpUrlError;

    #[test]
    fn account_name() {
        let error = TotpUrlError::AccountName("Laziz:".to_string());
        assert_eq!(
            error.to_string(),
            "Account Name can't contain a colon. \"Laziz:\" contains a colon"
        )
    }

    #[test]
    fn account_name_decoding() {
        let error = TotpUrlError::AccountNameDecoding("Laz&iz".to_string());
        assert_eq!(
            error.to_string(),
            "Couldn't URL decode \"Laz&iz\"".to_string()
        )
    }

    #[test]
    fn algorithm() {
        let error = TotpUrlError::Algorithm("SIKE".to_string());
        assert_eq!(
            error.to_string(),
            "Algorithm can only be SHA1, SHA256 or SHA512, not \"SIKE\"".to_string()
        )
    }

    #[test]
    fn digits() {
        let error = TotpUrlError::Digits("six".to_string());
        assert_eq!(
            error.to_string(),
            "Could not parse \"six\" as a number.".to_string()
        )
    }

    #[test]
    fn digits_number() {
        let error = TotpUrlError::DigitsNumber(5);
        assert_eq!(error.to_string(), "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. 5 digits is not allowed".to_string())
    }

    #[test]
    fn host() {
        let error = TotpUrlError::Host("hotp".to_string());
        assert_eq!(
            error.to_string(),
            "Host should be totp, not \"hotp\"".to_string()
        )
    }

    #[test]
    fn issuer() {
        let error = TotpUrlError::Issuer("Iss:uer".to_string());
        assert_eq!(
            error.to_string(),
            "Issuer can't contain a colon. \"Iss:uer\" contains a colon".to_string()
        )
    }

    #[test]
    fn issuer_decoding() {
        let error = TotpUrlError::IssuerDecoding("iss&uer".to_string());
        assert_eq!(
            error.to_string(),
            "Couldn't URL decode \"iss&uer\"".to_string()
        )
    }

    #[test]
    fn issuer_mismatch() {
        let error = TotpUrlError::IssuerMistmatch("Google".to_string(), "Github".to_string());
        assert_eq!(error.to_string(), "An issuer \"Google\" could be retrieved from the path, but a different issuer \"Github\" was found in the issuer URL parameter".to_string())
    }

    #[test]
    fn scheme() {
        let error = TotpUrlError::Scheme("https".to_string());
        assert_eq!(
            error.to_string(),
            "Scheme should be otpauth, not \"https\"".to_string()
        )
    }

    #[test]
    fn secret() {
        let error = TotpUrlError::Secret("YoLo".to_string());
        assert_eq!(
            error.to_string(),
            "Secret \"YoLo\" is not a valid non-padded base32 string".to_string()
        )
    }

    #[test]
    fn secret_size() {
        let error = TotpUrlError::SecretSize(112);
        assert_eq!(
            error.to_string(),
            "The length of the shared secret MUST be at least 128 bits. 112 bits is not enough"
                .to_string()
        )
    }

    #[test]
    #[cfg(feature = "otpauth")]
    fn step() {
        let error = TotpUrlError::Url(url::ParseError::EmptyHost);
        assert_eq!(
            error.to_string(),
            "Error parsing URL: empty host".to_string()
        )
    }
}