diff --git a/crates/bitwarden/src/vault/totp.rs b/crates/bitwarden/src/vault/totp.rs index 9bc92229b..6e863d57b 100644 --- a/crates/bitwarden/src/vault/totp.rs +++ b/crates/bitwarden/src/vault/totp.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr}; use crate::error::{Error, Result}; use chrono::{DateTime, Utc}; -use data_encoding::BASE32; +use data_encoding::BASE32_NOPAD; use hmac::{Hmac, Mac}; use reqwest::Url; use schemars::JsonSchema; @@ -12,6 +12,7 @@ type HmacSha1 = Hmac; type HmacSha256 = Hmac; type HmacSha512 = Hmac; +const BASE32_CHARS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY"; const DEFAULT_ALGORITHM: Algorithm = Algorithm::Sha1; @@ -128,9 +129,16 @@ impl FromStr for Totp { /// - Steam URI fn from_str(key: &str) -> Result { fn decode_secret(secret: &str) -> Result> { - BASE32 - .decode(secret.to_uppercase().as_bytes()) - .map_err(|_| "Unable to decode secret".into()) + // Sanitize the secret to only contain allowed characters + let secret = secret + .to_uppercase() + .chars() + .filter(|c| BASE32_CHARS.contains(*c)) + .collect::(); + + BASE32_NOPAD + .decode(secret.as_bytes()) + .map_err(|e| e.to_string().into()) } let params = if key.starts_with("otpauth://") { @@ -218,30 +226,26 @@ mod tests { #[test] fn test_generate_totp() { - let key = "WQIQ25BRKZYCJVYP".to_string(); - let time = Some( - DateTime::parse_from_rfc3339("2023-01-01T00:00:00.000Z") - .unwrap() - .with_timezone(&Utc), - ); - let response = generate_totp(key, time).unwrap(); - - assert_eq!(response.code, "194506".to_string()); - assert_eq!(response.period, 30); - } + let cases = vec![ + ("WQIQ25BRKZYCJVYP", "194506"), // valid base32 + ("wqiq25brkzycjvyp", "194506"), // lowercase + ("PIUDISEQYA", "829846"), // non padded + ("PIUDISEQYA======", "829846"), // padded + ("PIUD1IS!EQYA=", "829846"), // sanitized + ]; - #[test] - fn test_lowercase_secret() { - let key = "wqiq25brkzycjvyp".to_string(); let time = Some( DateTime::parse_from_rfc3339("2023-01-01T00:00:00.000Z") .unwrap() .with_timezone(&Utc), ); - let response = generate_totp(key, time).unwrap(); - assert_eq!(response.code, "194506".to_string()); - assert_eq!(response.period, 30); + for (key, expected_code) in cases { + let response = generate_totp(key.to_string(), time).unwrap(); + + assert_eq!(response.code, expected_code, "wrong code for key: {key}"); + assert_eq!(response.period, 30); + } } #[test]