use scraper::{Html, Selector};
use steam_totp::{generate_auth_code, generate_device_id, Secret};
use crate::{
client::SteamUser,
endpoint::steam_endpoint,
error::SteamUserError,
types::{SteamGuardStatus, TwoFactorResponse},
};
impl SteamUser {
#[steam_endpoint(POST, host = Api, path = "/ITwoFactorService/AddAuthenticator/v1/", kind = Auth)]
pub async fn enable_two_factor(&self) -> Result<TwoFactorResponse, SteamUserError> {
let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
let device_id = generate_device_id(steam_id, None);
let access_token = self.session.mobile_access_token.as_ref().ok_or(SteamUserError::MissingCredential { field: "mobile_access_token" })?.clone();
let response: serde_json::Value = self.post_path("/ITwoFactorService/AddAuthenticator/v1/").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).form(&[("steamid", steam_id.steam_id64().to_string()), ("authenticator_type", "1".to_string()), ("device_identifier", device_id), ("sms_phone_id", "1".to_string()), ("version", "2".to_string())]).send().await?.json().await?;
let response = response.get("response").ok_or_else(|| SteamUserError::MalformedResponse("Missing response object".into()))?;
let resp: TwoFactorResponse = serde_json::from_value(response.clone())?;
if let Some(ref shared_secret) = resp.shared_secret {
*self.session.shared_secret.lock() = Some(shared_secret.clone());
}
if resp.status != 1 {
return Err(SteamUserError::from_eresult(resp.status));
}
Ok(resp)
}
#[tracing::instrument(skip(self))]
pub async fn add_authenticator(&self) -> Result<TwoFactorResponse, SteamUserError> {
self.enable_two_factor().await
}
#[tracing::instrument(skip(self, shared_secret, activation_code))]
pub async fn finalize_two_factor(&self, shared_secret: &str, activation_code: &str) -> Result<(), SteamUserError> {
*self.session.shared_secret.lock() = Some(shared_secret.to_string());
self.finalize_authenticator(activation_code).await
}
#[steam_endpoint(POST, host = Api, path = "/ITwoFactorService/FinalizeAddAuthenticator/v1/", kind = Auth)]
pub async fn finalize_authenticator(&self, activation_code: &str) -> Result<(), SteamUserError> {
let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
let access_token = self.session.mobile_access_token.as_ref().ok_or(SteamUserError::MissingCredential { field: "mobile_access_token" })?.clone();
let shared_secret = self.session.shared_secret.lock().as_ref().ok_or(SteamUserError::MissingCredential { field: "shared_secret" })?.clone();
let mut time_offset = self.time_offset.lock().unwrap_or(0);
let mut attempts_left = 30;
while attempts_left > 0 {
let current_time = i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs(),
)
.unwrap_or(0);
let authenticator_time = current_time + time_offset;
let secret = Secret::from_string(&shared_secret)?;
let authenticator_code = generate_auth_code(&secret, time_offset)?;
let response: serde_json::Value = self.post_path("/ITwoFactorService/FinalizeAddAuthenticator/v1/").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).form(&[("steamid", steam_id.steam_id64().to_string()), ("authenticator_code", authenticator_code), ("authenticator_time", authenticator_time.to_string()), ("activation_code", activation_code.to_string())]).send().await?.json().await?;
let response = response.get("response").ok_or_else(|| SteamUserError::MalformedResponse("Missing response object".into()))?;
if let Some(server_time) = response.get("server_time").and_then(|v| v.as_i64()) {
time_offset = server_time - current_time;
*self.time_offset.lock() = Some(time_offset);
}
let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
if success {
return Ok(());
}
let status = i32::try_from(response.get("status").and_then(|v| v.as_i64()).unwrap_or(0)).unwrap_or(0);
if status == 89 {
return Err(SteamUserError::TwoFactorError("Invalid activation code".into()));
}
attempts_left -= 1;
time_offset += 30;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
Err(SteamUserError::TwoFactorError("Failed to finalize adding authenticator after 30 attempts".into()))
}
#[steam_endpoint(POST, host = Api, path = "/ITwoFactorService/RemoveAuthenticator/v1/", kind = Auth)]
pub async fn disable_two_factor(&self, revocation_code: &str) -> Result<(), SteamUserError> {
let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
let access_token = self.session.mobile_access_token.as_ref().ok_or(SteamUserError::MissingCredential { field: "mobile_access_token" })?;
let steam_id_str = steam_id.steam_id64().to_string();
let response: serde_json::Value = self.post_path("/ITwoFactorService/RemoveAuthenticator/v1/").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).form(&[("steamid", steam_id_str.as_str()), ("revocation_code", revocation_code), ("steamguard_scheme", "1")]).send().await?.json().await?;
let response = response.get("response").ok_or_else(|| SteamUserError::MalformedResponse("Missing response object".into()))?;
let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
if !success {
if let Some(status) = response.get("status").and_then(|v| v.as_i64()) {
return Err(SteamUserError::from_eresult(i32::try_from(status).unwrap_or(0)));
}
return Err(SteamUserError::TwoFactorError("Failed to remove authenticator".into()));
}
Ok(())
}
#[tracing::instrument(skip(self, revocation_code))]
pub async fn remove_authenticator(&self, revocation_code: &str) -> Result<(), SteamUserError> {
self.disable_two_factor(revocation_code).await
}
#[steam_endpoint(POST, host = Store, path = "/twofactor/manage_action", kind = Auth)]
pub async fn deauthorize_devices(&self) -> Result<(), SteamUserError> {
let response: serde_json::Value = self.post_path("/twofactor/manage_action").header("origin", "https://store.steampowered.com").header("referer", "https://store.steampowered.com/twofactor/manage").form(&[("action", "deauthorize")]).send().await?.json().await?;
if response.is_null() {
return Err(SteamUserError::MalformedResponse("Failed to deauthorize devices".into()));
}
Ok(())
}
#[steam_endpoint(GET, host = Store, path = "/twofactor/manage_action", kind = Read)]
pub async fn get_steam_guard_status(&self) -> Result<SteamGuardStatus, SteamUserError> {
let response = self.get_path("/twofactor/manage_action").send().await?.text().await?;
let document = Html::parse_document(&response);
let mobile_selector = Selector::parse("#steam_authenticator_form #steam_authenticator_check[checked]").expect("valid CSS selector");
let email_selector = Selector::parse("#email_authenticator_form #email_authenticator_check[checked]").expect("valid CSS selector");
let none_selector = Selector::parse("#none_authenticator_form #none_authenticator_check[checked]").expect("valid CSS selector");
if document.select(&mobile_selector).next().is_some() {
return Ok(SteamGuardStatus::Mobile);
}
if document.select(&email_selector).next().is_some() {
return Ok(SteamGuardStatus::Email);
}
if document.select(&none_selector).next().is_some() {
return Ok(SteamGuardStatus::None);
}
Err(SteamUserError::MalformedResponse("Could not determine Steam Guard status".into()))
}
#[steam_endpoint(POST, host = Store, path = "/twofactor/manage_action", kind = Auth)]
pub async fn enable_steam_guard_email(&self) -> Result<bool, SteamUserError> {
let response = self.post_path("/twofactor/manage_action").header("referer", "https://store.steampowered.com/twofactor/manage").form(&[("action", "email"), ("email_authenticator_check", "on")]).send().await?.text().await?;
let document = Html::parse_document(&response);
let title_selector = Selector::parse("title").expect("valid CSS selector");
let check_selector = Selector::parse("#email_authenticator_check[checked]").expect("valid CSS selector");
let title_ok = document.select(&title_selector).next().map(|t| t.text().collect::<String>() == "Steam Guard Mobile Authenticator").unwrap_or(false);
let checked = document.select(&check_selector).next().is_some();
Ok(title_ok && checked)
}
#[steam_endpoint(POST, host = Store, path = "/twofactor/manage_action", kind = Auth)]
pub async fn disable_steam_guard_email(&self) -> Result<bool, SteamUserError> {
let response = self.post_path("/twofactor/manage_action").header("referer", "https://store.steampowered.com/twofactor/manage_action").form(&[("action", "actuallynone")]).send().await?.text().await?;
let document = Html::parse_document(&response);
let title_selector = Selector::parse("title").expect("valid CSS selector");
let title_ok = document.select(&title_selector).next().map(|t| t.text().collect::<String>() == "Steam Guard Mobile Authenticator").unwrap_or(false);
let text_ok = response.contains("Turning Steam Guard off requires confirmation. We've sent you an email with a link to confirm disabling Steam Guard.");
Ok(title_ok && text_ok)
}
}