Merge pull request #47 from timvisee/feature-steam

Add Steam algorithm behind 'steam' feature
This commit is contained in:
Cléo Rebert 2023-01-06 10:10:01 +01:00 committed by GitHub
commit d5b94dc0df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 15 deletions

View File

@ -21,6 +21,7 @@ otpauth = ["url", "urlencoding"]
qr = ["qrcodegen", "image", "base64", "otpauth"] qr = ["qrcodegen", "image", "base64", "otpauth"]
serde_support = ["serde"] serde_support = ["serde"]
gen_secret = ["rand"] gen_secret = ["rand"]
steam = []
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }

View File

@ -19,6 +19,8 @@ With optional feature "serde_support", library-defined types `TOTP` and `Algorit
With optional feature "gen_secret", a secret will be generated for you to store in database. With optional feature "gen_secret", a secret will be generated for you to store in database.
### zeroize ### zeroize
Securely zero secret information when the TOTP struct is dropped. Securely zero secret information when the TOTP struct is dropped.
### steam
Add support for Steam TOTP tokens.
# Examples # Examples

43
src/custom_providers.rs Normal file
View File

@ -0,0 +1,43 @@
#[cfg(feature = "steam")]
use crate::{Algorithm, TOTP};
#[cfg(feature = "steam")]
impl TOTP {
#[cfg(feature = "otpauth")]
/// Will create a new instance of TOTP using the Steam algorithm with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values
///
/// # Description
/// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)`
///
/// ```rust
/// use totp_rs::{Secret, TOTP};
/// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".to_string());
/// let totp = TOTP::new_steam(secret.to_bytes().unwrap(), Some("username".to_string()));
/// ```
pub fn new_steam(secret: Vec<u8>, account_name: String) -> TOTP {
Self::new_unchecked(
Algorithm::Steam,
5,
1,
30,
secret,
Some("Steam".into()),
account_name,
)
}
#[cfg(not(feature = "otpauth"))]
/// Will create a new instance of TOTP using the Steam algorithm with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values
///
/// # Description
/// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)`
///
/// ```rust
/// use totp_rs::{Secret, TOTP};
/// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".to_string());
/// let totp = TOTP::new_steam(secret.to_bytes().unwrap());
/// ```
pub fn new_steam(secret: Vec<u8>) -> TOTP {
Self::new_unchecked(Algorithm::Steam, 5, 1, 30, secret)
}
}

View File

@ -47,6 +47,7 @@
//! # } //! # }
//! ``` //! ```
mod custom_providers;
mod rfc; mod rfc;
mod secret; mod secret;
mod url_error; mod url_error;
@ -75,6 +76,10 @@ type HmacSha1 = hmac::Hmac<sha1::Sha1>;
type HmacSha256 = hmac::Hmac<sha2::Sha256>; type HmacSha256 = hmac::Hmac<sha2::Sha256>;
type HmacSha512 = hmac::Hmac<sha2::Sha512>; type HmacSha512 = hmac::Hmac<sha2::Sha512>;
/// Alphabet for Steam tokens.
#[cfg(feature = "steam")]
const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY";
/// Algorithm enum holds the three standards algorithms for TOTP as per the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A) /// Algorithm enum holds the three standards algorithms for TOTP as per the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A)
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
@ -82,6 +87,8 @@ pub enum Algorithm {
SHA1, SHA1,
SHA256, SHA256,
SHA512, SHA512,
#[cfg(feature = "steam")]
Steam,
} }
impl std::default::Default for Algorithm { impl std::default::Default for Algorithm {
@ -96,6 +103,8 @@ impl fmt::Display for Algorithm {
Algorithm::SHA1 => f.write_str("SHA1"), Algorithm::SHA1 => f.write_str("SHA1"),
Algorithm::SHA256 => f.write_str("SHA256"), Algorithm::SHA256 => f.write_str("SHA256"),
Algorithm::SHA512 => f.write_str("SHA512"), Algorithm::SHA512 => f.write_str("SHA512"),
#[cfg(feature = "steam")]
Algorithm::Steam => f.write_str("SHA1"),
} }
} }
} }
@ -114,6 +123,8 @@ impl Algorithm {
Algorithm::SHA1 => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data), Algorithm::SHA1 => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
Algorithm::SHA256 => Algorithm::hash(HmacSha256::new_from_slice(key).unwrap(), data), Algorithm::SHA256 => Algorithm::hash(HmacSha256::new_from_slice(key).unwrap(), data),
Algorithm::SHA512 => Algorithm::hash(HmacSha512::new_from_slice(key).unwrap(), data), Algorithm::SHA512 => Algorithm::hash(HmacSha512::new_from_slice(key).unwrap(), data),
#[cfg(feature = "steam")]
Algorithm::Steam => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
} }
} }
} }
@ -384,13 +395,28 @@ impl TOTP {
pub fn generate(&self, time: u64) -> String { pub fn generate(&self, time: u64) -> String {
let result: &[u8] = &self.sign(time); let result: &[u8] = &self.sign(time);
let offset = (result.last().unwrap() & 15) as usize; let offset = (result.last().unwrap() & 15) as usize;
let result = #[allow(unused_mut)]
let mut result =
u32::from_be_bytes(result[offset..offset + 4].try_into().unwrap()) & 0x7fff_ffff; u32::from_be_bytes(result[offset..offset + 4].try_into().unwrap()) & 0x7fff_ffff;
format!(
"{1:00$}", match self.algorithm {
self.digits, Algorithm::SHA1 | Algorithm::SHA256 | Algorithm::SHA512 => format!(
result % 10_u32.pow(self.digits as u32) "{1:00$}",
) self.digits,
result % 10_u32.pow(self.digits as u32)
),
#[cfg(feature = "steam")]
Algorithm::Steam => (0..self.digits)
.map(|_| {
let c = STEAM_CHARS
.chars()
.nth(result as usize % STEAM_CHARS.len())
.unwrap();
result /= STEAM_CHARS.len() as u32;
c
})
.collect(),
}
} }
/// Returns the timestamp of the first second for the next step /// Returns the timestamp of the first second for the next step
@ -476,14 +502,6 @@ impl TOTP {
fn parts_from_url<S: AsRef<str>>( fn parts_from_url<S: AsRef<str>>(
url: S, url: S,
) -> Result<(Algorithm, usize, u8, u64, Vec<u8>, Option<String>, String), TotpUrlError> { ) -> Result<(Algorithm, usize, u8, u64, Vec<u8>, Option<String>, String), TotpUrlError> {
let url = Url::parse(url.as_ref()).map_err(TotpUrlError::Url)?;
if url.scheme() != "otpauth" {
return Err(TotpUrlError::Scheme(url.scheme().to_string()));
}
if url.host() != Some(Host::Domain("totp")) {
return Err(TotpUrlError::Host(url.host().unwrap().to_string()));
}
let mut algorithm = Algorithm::SHA1; let mut algorithm = Algorithm::SHA1;
let mut digits = 6; let mut digits = 6;
let mut step = 30; let mut step = 30;
@ -491,6 +509,22 @@ impl TOTP {
let mut issuer: Option<String> = None; let mut issuer: Option<String> = None;
let mut account_name: String; let mut account_name: String;
let url = Url::parse(url.as_ref()).map_err(TotpUrlError::Url)?;
if url.scheme() != "otpauth" {
return Err(TotpUrlError::Scheme(url.scheme().to_string()));
}
match url.host() {
Some(Host::Domain("totp")) => {}
#[cfg(feature = "steam")]
Some(Host::Domain("steam")) => {
algorithm = Algorithm::Steam;
digits = 5;
}
_ => {
return Err(TotpUrlError::Host(url.host().unwrap().to_string()));
}
}
let path = url.path().trim_start_matches('/'); let path = url.path().trim_start_matches('/');
if path.contains(':') { if path.contains(':') {
let parts = path.split_once(':').unwrap(); let parts = path.split_once(':').unwrap();
@ -510,6 +544,10 @@ impl TOTP {
for (key, value) in url.query_pairs() { for (key, value) in url.query_pairs() {
match key.as_ref() { match key.as_ref() {
#[cfg(feature = "steam")]
"algorithm" if algorithm == Algorithm::Steam => {
// Do not change used algorithm if this is Steam
}
"algorithm" => { "algorithm" => {
algorithm = match value.as_ref() { algorithm = match value.as_ref() {
"SHA1" => Algorithm::SHA1, "SHA1" => Algorithm::SHA1,
@ -535,6 +573,12 @@ impl TOTP {
) )
.ok_or_else(|| TotpUrlError::Secret(value.to_string()))?; .ok_or_else(|| TotpUrlError::Secret(value.to_string()))?;
} }
#[cfg(feature = "steam")]
"issuer" if value.to_lowercase() == "steam" => {
algorithm = Algorithm::Steam;
digits = 5;
issuer = Some(value.into());
}
"issuer" => { "issuer" => {
let param_issuer = value let param_issuer = value
.parse::<String>() .parse::<String>()
@ -564,6 +608,12 @@ impl TOTP {
/// Secret will be base 32'd without padding, as per RFC. /// Secret will be base 32'd without padding, as per RFC.
#[cfg(feature = "otpauth")] #[cfg(feature = "otpauth")]
pub fn get_url(&self) -> String { pub fn get_url(&self) -> String {
#[allow(unused_mut)]
let mut host = "totp";
#[cfg(feature = "steam")]
if self.algorithm == Algorithm::Steam {
host = "steam";
}
let account_name: String = urlencoding::encode(self.account_name.as_str()).to_string(); let account_name: String = urlencoding::encode(self.account_name.as_str()).to_string();
let mut label: String = format!("{}?", account_name); let mut label: String = format!("{}?", account_name);
if self.issuer.is_some() { if self.issuer.is_some() {
@ -573,7 +623,8 @@ impl TOTP {
} }
format!( format!(
"otpauth://totp/{}secret={}&digits={}&algorithm={}", "otpauth://{}/{}secret={}&digits={}&algorithm={}",
host,
label, label,
self.get_secret_base32(), self.get_secret_base32(),
self.digits, self.digits,