From 31a888c0e4f3d8635bb646bbbde1326c839ab8bd Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Wed, 4 Aug 2021 21:26:14 -0400 Subject: [PATCH 01/24] add NetworkFailure to AccountLinkError --- src/main.rs | 5 +-- {src => steamguard/src}/accountlinker.rs | 55 ++++++++++++++++++++++-- steamguard/src/lib.rs | 2 + 3 files changed, 55 insertions(+), 7 deletions(-) rename {src => steamguard/src}/accountlinker.rs (64%) diff --git a/src/main.rs b/src/main.rs index 2bb1860..f749142 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::{ sync::{Arc, Mutex}, }; use steamguard::{ - steamapi, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, UserLogin, + steamapi, AccountLinker, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, UserLogin, }; use termion::{ event::{Event, Key}, @@ -22,7 +22,6 @@ use termion::{ extern crate lazy_static; #[macro_use] extern crate anyhow; -mod accountlinker; mod accountmanager; lazy_static! { @@ -124,7 +123,7 @@ fn main() { if matches.is_present("setup") { info!("setup"); - let mut linker = accountlinker::AccountLinker::new(); + let mut linker = AccountLinker::new(); // do_login(&mut linker.account); // linker.link(linker.account.session.expect("no login session")); return; diff --git a/src/accountlinker.rs b/steamguard/src/accountlinker.rs similarity index 64% rename from src/accountlinker.rs rename to steamguard/src/accountlinker.rs index 5daf521..f09dfca 100644 --- a/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,15 +1,18 @@ +use crate::{steamapi::Session, SteamGuardAccount}; use log::*; use reqwest::{cookie::CookieStore, header::COOKIE, Url}; use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; -use steamguard::{steamapi::Session, SteamGuardAccount}; +use std::error::Error; +use std::fmt::Display; #[derive(Debug, Clone)] pub struct AccountLinker { device_id: String, phone_number: String, pub account: SteamGuardAccount, + pub finalized: bool, client: reqwest::blocking::Client, } @@ -19,6 +22,7 @@ impl AccountLinker { device_id: generate_device_id(), phone_number: String::from(""), account: SteamGuardAccount::new(), + finalized: false, client: reqwest::blocking::ClientBuilder::new() .cookie_store(true) .build() @@ -26,15 +30,29 @@ impl AccountLinker { }; } - pub fn link(&self, session: &mut Session) { + pub fn link( + &self, + session: &mut Session, + ) -> anyhow::Result { let mut params = HashMap::new(); params.insert("access_token", session.token.clone()); params.insert("steamid", session.steam_id.to_string()); params.insert("device_identifier", self.device_id.clone()); - params.insert("authenticator_type", String::from("1")); - params.insert("sms_phone_id", String::from("1")); + params.insert("authenticator_type", "1".into()); + params.insert("sms_phone_id", "1".into()); + + let resp: AddAuthenticatorResponse = self + .client + .post("https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v0001") + .form(¶ms) + .send()? + .json()?; + + return Err(AccountLinkError::Unknown); } + pub fn finalize(&self, session: &Session) {} + fn has_phone(&self, session: &Session) -> bool { return self._phoneajax(session, "has_phone", "null"); } @@ -85,3 +103,32 @@ fn generate_device_id() -> String { pub struct AddAuthenticatorResponse { pub response: SteamGuardAccount, } + +#[derive(Debug)] +pub enum AccountLinkError { + /// No phone number on the account + MustProvidePhoneNumber, + /// A phone number is already on the account + MustRemovePhoneNumber, + /// User need to click link from confirmation email + MustConfirmEmail, + /// Must provide an SMS code + AwaitingFinalization, + AuthenticatorPresent, + NetworkFailure(reqwest::Error), + Unknown, +} + +impl Display for AccountLinkError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "{:?}", self) + } +} + +impl Error for AccountLinkError {} + +impl From for AccountLinkError { + fn from(err: reqwest::Error) -> AccountLinkError { + AccountLinkError::NetworkFailure(err) + } +} diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index ebfbdf0..8c91d3a 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -1,3 +1,4 @@ +pub use accountlinker::{AccountLinkError, AccountLinker, AddAuthenticatorResponse}; use anyhow::Result; pub use confirmation::{Confirmation, ConfirmationType}; use hmacsha1::hmac_sha1; @@ -18,6 +19,7 @@ extern crate anyhow; #[macro_use] extern crate maplit; +mod accountlinker; mod confirmation; pub mod steamapi; mod userlogin; From e4d7ea4475ff88bd6fb951f2b68e3e0e7f741ba3 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 8 Aug 2021 13:37:18 -0400 Subject: [PATCH 02/24] move phoneajax methods to steamapi module --- src/main.rs | 3 +- steamguard/src/accountlinker.rs | 44 ---------------------------- steamguard/src/steamapi.rs | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/src/main.rs b/src/main.rs index f749142..cb5b979 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,8 @@ use std::{ sync::{Arc, Mutex}, }; use steamguard::{ - steamapi, AccountLinker, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, UserLogin, + steamapi, AccountLinker, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, + UserLogin, }; use termion::{ event::{Event, Key}, diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index f09dfca..e9da246 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,8 +1,5 @@ use crate::{steamapi::Session, SteamGuardAccount}; -use log::*; -use reqwest::{cookie::CookieStore, header::COOKIE, Url}; use serde::Deserialize; -use serde_json::Value; use std::collections::HashMap; use std::error::Error; use std::fmt::Display; @@ -52,47 +49,6 @@ impl AccountLinker { } pub fn finalize(&self, session: &Session) {} - - fn has_phone(&self, session: &Session) -> bool { - return self._phoneajax(session, "has_phone", "null"); - } - - fn _phoneajax(&self, session: &Session, op: &str, arg: &str) -> bool { - trace!("_phoneajax: op={}", op); - let url = "https://steamcommunity.com".parse::().unwrap(); - let cookies = reqwest::cookie::Jar::default(); - cookies.add_cookie_str("mobileClientVersion=0 (2.1.3)", &url); - cookies.add_cookie_str("mobileClient=android", &url); - cookies.add_cookie_str("Steam_Language=english", &url); - - let mut params = HashMap::new(); - params.insert("op", op); - params.insert("arg", arg); - params.insert("sessionid", session.session_id.as_str()); - if op == "check_sms_code" { - params.insert("checkfortos", "0"); - params.insert("skipvoip", "1"); - } - - let resp = self - .client - .post("https://steamcommunity.com/steamguard/phoneajax") - .header(COOKIE, cookies.cookies(&url).unwrap()) - .send() - .unwrap(); - - let result: Value = resp.json().unwrap(); - if result["has_phone"] != Value::Null { - trace!("found has_phone field"); - return result["has_phone"].as_bool().unwrap(); - } else if result["success"] != Value::Null { - trace!("found success field"); - return result["success"].as_bool().unwrap(); - } else { - trace!("did not find any expected field"); - return false; - } - } } fn generate_device_id() -> String { diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 209cfeb..0fda929 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -7,6 +7,7 @@ use reqwest::{ Url, }; use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; use std::iter::FromIterator; use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; @@ -289,6 +290,56 @@ impl SteamApiClient { } } } + + /// Endpoint: POST /steamguard/phoneajax + fn phoneajax(&self, op: &str, arg: &str) -> anyhow::Result { + let mut params = hashmap! { + "op" => op, + "arg" => arg, + "sessionid" => self.session.as_ref().unwrap().session_id.as_str(), + }; + if op == "check_sms_code" { + params.insert("checkfortos", "0"); + params.insert("skipvoip", "1"); + } + + let resp = self + .post("https://steamcommunity.com/steamguard/phoneajax") + .form(¶ms) + .send()?; + + let result: Value = resp.json()?; + if result["has_phone"] != Value::Null { + trace!("found has_phone field"); + return result["has_phone"] + .as_bool() + .ok_or(anyhow!("failed to parse has_phone field into boolean")); + } else if result["success"] != Value::Null { + trace!("found success field"); + return result["success"] + .as_bool() + .ok_or(anyhow!("failed to parse success field into boolean")); + } else { + trace!("did not find any expected field"); + return Ok(false); + } + } + + pub fn has_phone(&self) -> anyhow::Result { + return self.phoneajax("has_phone", "null"); + } + + pub fn check_sms_code(&self, sms_code: String) -> anyhow::Result { + return self.phoneajax("check_sms_code", sms_code.as_str()); + } + + pub fn check_email_confirmation(&self) -> anyhow::Result { + return self.phoneajax("email_confirmation", ""); + } + + pub fn add_phone_number(&self, phone_number: String) -> anyhow::Result { + return self.phoneajax("add_phone_number", phone_number.as_str()); + } } #[test] From 2e4058cfca52828a0b18daa48f1b41dd37e968aa Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 8 Aug 2021 15:25:27 -0400 Subject: [PATCH 03/24] add add_authenticator to steamapi module --- steamguard/src/accountlinker.rs | 11 ++--- steamguard/src/lib.rs | 2 + steamguard/src/steamapi.rs | 82 +++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index e9da246..6359d13 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,5 +1,7 @@ -use crate::{steamapi::Session, SteamGuardAccount}; -use serde::Deserialize; +use crate::{ + steamapi::{AddAuthenticatorResponse, Session}, + SteamGuardAccount, +}; use std::collections::HashMap; use std::error::Error; use std::fmt::Display; @@ -55,11 +57,6 @@ fn generate_device_id() -> String { return format!("android:{}", uuid::Uuid::new_v4().to_string()); } -#[derive(Debug, Clone, Deserialize)] -pub struct AddAuthenticatorResponse { - pub response: SteamGuardAccount, -} - #[derive(Debug)] pub enum AccountLinkError { /// No phone number on the account diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index 8c91d3a..19b1a29 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -47,6 +47,7 @@ pub struct SteamGuardAccount { pub uri: String, pub fully_enrolled: bool, pub device_id: String, + pub secret_1: String, #[serde(rename = "Session")] pub session: Option, } @@ -84,6 +85,7 @@ impl SteamGuardAccount { uri: String::from(""), fully_enrolled: false, device_id: String::from(""), + secret_1: "".into(), session: Option::None, }; } diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 0fda929..a429610 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -12,8 +12,11 @@ use std::iter::FromIterator; use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::SteamGuardAccount; + lazy_static! { static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::().unwrap(); + static ref STEAM_API_BASE: String = "https://api.steampowered.com".into(); } #[derive(Debug, Clone, Deserialize)] @@ -340,6 +343,37 @@ impl SteamApiClient { pub fn add_phone_number(&self, phone_number: String) -> anyhow::Result { return self.phoneajax("add_phone_number", phone_number.as_str()); } + + /// Starts the authenticator linking process. + /// This doesn't check any prereqisites to ensure the request will pass validation on Steam's side (eg. sms/email confirmations). + /// A valid `Session` is required for this request. Cookies are not needed for this request, but they are set anyway. + /// + /// Host: api.steampowered.com + /// Endpoint: POST /ITwoFactorService/AddAuthenticator/v0001 + pub fn add_authenticator(&self, device_id: String) -> anyhow::Result { + ensure!(matches!(self.session, Some(_))); + let params = hashmap! { + "access_token" => self.session.as_ref().unwrap().token.clone(), + "steamid" => self.session.as_ref().unwrap().steam_id.to_string(), + "authenticator_type" => "1".into(), + "device_identifier" => device_id, + "sms_phone_id" => "1".into(), + }; + + let text = self + .post(format!( + "{}/ITwoFactorService/AddAuthenticator/v0001", + STEAM_API_BASE.to_string() + )) + .form(¶ms) + .send()? + .text()?; + trace!("raw login response: {}", text); + + let resp: AddAuthenticatorResponse = serde_json::from_str(text.as_str())?; + + Ok(resp) + } } #[test] @@ -380,3 +414,51 @@ fn test_login_response_parse() { ); assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5"); } + +#[derive(Debug, Clone, Deserialize)] +pub struct AddAuthenticatorResponse { + pub response: AddAuthenticatorResponseInner, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AddAuthenticatorResponseInner { + /// Shared secret between server and authenticator + pub shared_secret: String, + /// Authenticator serial number (unique per token) + pub serial_number: String, + /// code used to revoke authenticator + pub revocation_code: String, + /// URI for QR code generation + pub uri: String, + /// Current server time + pub server_time: u64, + /// Account name to display on token client + pub account_name: String, + /// Token GID assigned by server + pub token_gid: String, + /// Secret used for identity attestation (e.g., for eventing) + pub identity_secret: String, + /// Spare shared secret + pub secret_1: String, + /// Result code + pub status: String, +} + +impl AddAuthenticatorResponse { + pub fn to_steam_guard_account(&self) -> SteamGuardAccount { + SteamGuardAccount { + shared_secret: self.response.shared_secret, + serial_number: self.response.serial_number, + revocation_code: self.response.revocation_code, + uri: self.response.uri, + server_time: self.response.server_time, + account_name: self.response.account_name, + token_gid: self.response.token_gid, + identity_secret: self.response.identity_secret, + secret_1: self.response.secret_1, + fully_enrolled: false, + device_id: "".into(), + session: None, + } + } +} From 58897b6695ba714c4c0f0728440b61be7b55aa80 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 8 Aug 2021 18:32:50 -0400 Subject: [PATCH 04/24] implement the first step for account linking process --- src/main.rs | 91 +++++++++++++++++++++++++++++-- steamguard/src/accountlinker.rs | 96 ++++++++++++++++++++++----------- steamguard/src/lib.rs | 5 +- steamguard/src/steamapi.rs | 50 +++++++++++++---- 4 files changed, 195 insertions(+), 47 deletions(-) diff --git a/src/main.rs b/src/main.rs index cb5b979..e284634 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,13 +120,49 @@ fn main() { } } - manifest.load_accounts(); + manifest + .load_accounts() + .expect("Failed to load accounts in manifest"); if matches.is_present("setup") { - info!("setup"); - let mut linker = AccountLinker::new(); - // do_login(&mut linker.account); - // linker.link(linker.account.session.expect("no login session")); + println!("Log in to the account that you want to link to steamguard-cli"); + let session = do_login_raw().expect("Failed to log in. Account has not been linked."); + + let mut linker = AccountLinker::new(session); + let account: SteamGuardAccount; + loop { + match linker.link() { + Ok(a) => { + account = a; + break; + } + Err(err) => { + error!( + "Failed to link authenticator. Account has not been linked. {}", + err + ); + return; + } + } + } + manifest.add_account(account); + match manifest.save() { + Ok(_) => {} + Err(err) => { + error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); + println!( + "Just in case, here is the account info. Save it somewhere just in case!\n{:?}", + manifest.accounts.last().as_ref().unwrap() + ); + return; + } + } + + debug!("attempting link finalization"); + print!("Enter SMS code: "); + let sms_code = prompt(); + linker.finalize(sms_code); + return; } @@ -427,6 +463,51 @@ fn do_login(account: &mut SteamGuardAccount) { } } +fn do_login_raw() -> anyhow::Result { + print!("Username: "); + let username = prompt(); + let _ = std::io::stdout().flush(); + let password = rpassword::prompt_password_stdout("Password: ").unwrap(); + if password.len() > 0 { + debug!("password is present"); + } else { + debug!("password is empty"); + } + // TODO: reprompt if password is empty + let mut login = UserLogin::new(username, password); + let mut loops = 0; + loop { + match login.login() { + Ok(s) => { + return Ok(s); + } + Err(LoginError::Need2FA) => { + print!("Enter 2fa code: "); + let server_time = steamapi::get_server_time(); + login.twofactor_code = prompt(); + } + Err(LoginError::NeedCaptcha { captcha_gid }) => { + debug!("need captcha to log in"); + login.captcha_text = prompt_captcha_text(&captcha_gid); + } + Err(LoginError::NeedEmail) => { + println!("You should have received an email with a code."); + print!("Enter code: "); + login.email_code = prompt(); + } + Err(r) => { + error!("Fatal login result: {:?}", r); + bail!(r); + } + } + loops += 1; + if loops > 2 { + error!("Too many loops. Aborting login process, to avoid getting rate limited."); + bail!("Too many loops. Login process aborted to avoid getting rate limited."); + } + } +} + fn demo_confirmation_menu() { info!("showing demo menu"); let (accept, deny) = prompt_confirmation_menu(vec![ diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 6359d13..ff1e9c5 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,56 +1,84 @@ use crate::{ - steamapi::{AddAuthenticatorResponse, Session}, + steamapi::{AddAuthenticatorResponse, Session, SteamApiClient}, SteamGuardAccount, }; -use std::collections::HashMap; use std::error::Error; use std::fmt::Display; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct AccountLinker { device_id: String, phone_number: String, - pub account: SteamGuardAccount, + pub account: Option, pub finalized: bool, - client: reqwest::blocking::Client, + sent_confirmation_email: bool, + session: Session, + client: SteamApiClient, } impl AccountLinker { - pub fn new() -> AccountLinker { + pub fn new(session: Session) -> AccountLinker { return AccountLinker { device_id: generate_device_id(), - phone_number: String::from(""), - account: SteamGuardAccount::new(), + phone_number: "".into(), + account: None, finalized: false, - client: reqwest::blocking::ClientBuilder::new() - .cookie_store(true) - .build() - .unwrap(), + sent_confirmation_email: false, + session: session, + client: SteamApiClient::new(), }; } - pub fn link( - &self, - session: &mut Session, - ) -> anyhow::Result { - let mut params = HashMap::new(); - params.insert("access_token", session.token.clone()); - params.insert("steamid", session.steam_id.to_string()); - params.insert("device_identifier", self.device_id.clone()); - params.insert("authenticator_type", "1".into()); - params.insert("sms_phone_id", "1".into()); + pub fn link(&mut self) -> anyhow::Result { + let has_phone = self.client.has_phone()?; - let resp: AddAuthenticatorResponse = self - .client - .post("https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v0001") - .form(¶ms) - .send()? - .json()?; + if has_phone && !self.phone_number.is_empty() { + return Err(AccountLinkError::MustRemovePhoneNumber); + } + if !has_phone && self.phone_number.is_empty() { + return Err(AccountLinkError::MustProvidePhoneNumber); + } - return Err(AccountLinkError::Unknown); + if !has_phone { + if self.sent_confirmation_email { + if !self.client.check_email_confirmation()? { + return Err(AccountLinkError::Unknown(anyhow!( + "Failed email confirmation check" + ))); + } + } else if !self.client.add_phone_number(self.phone_number.clone())? { + return Err(AccountLinkError::Unknown(anyhow!( + "Failed to add phone number" + ))); + } else { + self.sent_confirmation_email = true; + return Err(AccountLinkError::MustConfirmEmail); + } + } + + let resp: AddAuthenticatorResponse = + self.client.add_authenticator(self.device_id.clone())?; + + match resp.response.status { + 29 => { + return Err(AccountLinkError::AuthenticatorPresent); + } + 1 => { + let mut account = resp.to_steam_guard_account(); + account.device_id = self.device_id.clone(); + account.session = self.client.session.clone(); + return Ok(account); + } + status => { + return Err(AccountLinkError::Unknown(anyhow!( + "Unknown add authenticator status code: {}", + status + ))); + } + } } - pub fn finalize(&self, session: &Session) {} + pub fn finalize(&self, account: &SteamGuardAccount, sms_code: String) {} } fn generate_device_id() -> String { @@ -69,7 +97,7 @@ pub enum AccountLinkError { AwaitingFinalization, AuthenticatorPresent, NetworkFailure(reqwest::Error), - Unknown, + Unknown(anyhow::Error), } impl Display for AccountLinkError { @@ -85,3 +113,9 @@ impl From for AccountLinkError { AccountLinkError::NetworkFailure(err) } } + +impl From for AccountLinkError { + fn from(err: anyhow::Error) -> AccountLinkError { + AccountLinkError::Unknown(err) + } +} diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index 19b1a29..8ea4f83 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -1,4 +1,4 @@ -pub use accountlinker::{AccountLinkError, AccountLinker, AddAuthenticatorResponse}; +pub use accountlinker::{AccountLinkError, AccountLinker}; use anyhow::Result; pub use confirmation::{Confirmation, ConfirmationType}; use hmacsha1::hmac_sha1; @@ -10,7 +10,8 @@ use reqwest::{ }; use scraper::{Html, Selector}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, convert::TryInto, thread, time}; +use std::{collections::HashMap, convert::TryInto}; +pub use steamapi::AddAuthenticatorResponse; pub use userlogin::{LoginError, UserLogin}; #[macro_use] extern crate lazy_static; diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index a429610..86f7901 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -374,6 +374,36 @@ impl SteamApiClient { Ok(resp) } + + /// + /// Host: api.steampowered.com + /// Endpoint: POST /ITwoFactorService/FinalizeAddAuthenticator/v0001 + pub fn finalize_authenticator( + &self, + sms_code: String, + code_2fa: String, + time_2fa: u64, + ) -> anyhow::Result { + ensure!(matches!(self.session, Some(_))); + let params = hashmap! { + "steamid" => self.session.as_ref().unwrap().steam_id.to_string(), + "access_token" => self.session.as_ref().unwrap().token.clone(), + "activation_code" => sms_code, + "authenticator_code" => code_2fa, + "authenticator_time" => time_2fa.to_string(), + }; + + let resp = self + .post(format!( + "{}/ITwoFactorService/FinalizeAddAuthenticator/v0001", + STEAM_API_BASE.to_string() + )) + .form(¶ms) + .send()? + .json()?; + + todo!(); + } } #[test] @@ -441,24 +471,26 @@ pub struct AddAuthenticatorResponseInner { /// Spare shared secret pub secret_1: String, /// Result code - pub status: String, + pub status: i32, } impl AddAuthenticatorResponse { pub fn to_steam_guard_account(&self) -> SteamGuardAccount { SteamGuardAccount { - shared_secret: self.response.shared_secret, - serial_number: self.response.serial_number, - revocation_code: self.response.revocation_code, - uri: self.response.uri, + shared_secret: self.response.shared_secret.clone(), + serial_number: self.response.serial_number.clone(), + revocation_code: self.response.revocation_code.clone(), + uri: self.response.uri.clone(), server_time: self.response.server_time, - account_name: self.response.account_name, - token_gid: self.response.token_gid, - identity_secret: self.response.identity_secret, - secret_1: self.response.secret_1, + account_name: self.response.account_name.clone(), + token_gid: self.response.token_gid.clone(), + identity_secret: self.response.identity_secret.clone(), + secret_1: self.response.secret_1.clone(), fully_enrolled: false, device_id: "".into(), session: None, } } } + +pub struct FinalizeAddAuthenticatorResponse {} From 38f41b144f8cb12c01bf7126aedabd2a5ee350f1 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 8 Aug 2021 19:09:15 -0400 Subject: [PATCH 05/24] refactor to use SteamApiResponse --- steamguard/src/accountlinker.rs | 2 +- steamguard/src/steamapi.rs | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index ff1e9c5..c285fcb 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -59,7 +59,7 @@ impl AccountLinker { let resp: AddAuthenticatorResponse = self.client.add_authenticator(self.device_id.clone())?; - match resp.response.status { + match resp.status { 29 => { return Err(AccountLinkError::AuthenticatorPresent); } diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 86f7901..cb1c7fa 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -370,9 +370,9 @@ impl SteamApiClient { .text()?; trace!("raw login response: {}", text); - let resp: AddAuthenticatorResponse = serde_json::from_str(text.as_str())?; + let resp: SteamApiResponse = serde_json::from_str(text.as_str())?; - Ok(resp) + Ok(resp.response) } /// @@ -393,7 +393,7 @@ impl SteamApiClient { "authenticator_time" => time_2fa.to_string(), }; - let resp = self + let resp: SteamApiResponse = self .post(format!( "{}/ITwoFactorService/FinalizeAddAuthenticator/v0001", STEAM_API_BASE.to_string() @@ -446,12 +446,12 @@ fn test_login_response_parse() { } #[derive(Debug, Clone, Deserialize)] -pub struct AddAuthenticatorResponse { - pub response: AddAuthenticatorResponseInner, +pub struct SteamApiResponse { + pub response: T } #[derive(Debug, Clone, Deserialize)] -pub struct AddAuthenticatorResponseInner { +pub struct AddAuthenticatorResponse { /// Shared secret between server and authenticator pub shared_secret: String, /// Authenticator serial number (unique per token) @@ -477,15 +477,15 @@ pub struct AddAuthenticatorResponseInner { impl AddAuthenticatorResponse { pub fn to_steam_guard_account(&self) -> SteamGuardAccount { SteamGuardAccount { - shared_secret: self.response.shared_secret.clone(), - serial_number: self.response.serial_number.clone(), - revocation_code: self.response.revocation_code.clone(), - uri: self.response.uri.clone(), - server_time: self.response.server_time, - account_name: self.response.account_name.clone(), - token_gid: self.response.token_gid.clone(), - identity_secret: self.response.identity_secret.clone(), - secret_1: self.response.secret_1.clone(), + shared_secret: self.shared_secret.clone(), + serial_number: self.serial_number.clone(), + revocation_code: self.revocation_code.clone(), + uri: self.uri.clone(), + server_time: self.server_time, + account_name: self.account_name.clone(), + token_gid: self.token_gid.clone(), + identity_secret: self.identity_secret.clone(), + secret_1: self.secret_1.clone(), fully_enrolled: false, device_id: "".into(), session: None, @@ -493,4 +493,5 @@ impl AddAuthenticatorResponse { } } +#[derive(Debug, Clone, Deserialize)] pub struct FinalizeAddAuthenticatorResponse {} From e0578fce39e20a7446358f0c94905a53e488eb02 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 8 Aug 2021 19:11:15 -0400 Subject: [PATCH 06/24] finish steamapi::finalize_authenticator --- steamguard/src/steamapi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index cb1c7fa..62bbe8a 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -402,7 +402,7 @@ impl SteamApiClient { .send()? .json()?; - todo!(); + return Ok(resp.response); } } From db2ec59c0713246b507cba9431d3e3741aa02493 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 00:09:34 -0400 Subject: [PATCH 07/24] add account link finalization --- src/main.rs | 33 +++++++++++-- steamguard/src/accountlinker.rs | 83 ++++++++++++++++++++++----------- steamguard/src/steamapi.rs | 13 ++++-- 3 files changed, 96 insertions(+), 33 deletions(-) diff --git a/src/main.rs b/src/main.rs index e284634..aba5c58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use std::{ }; use steamguard::{ steamapi, AccountLinker, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, - UserLogin, + UserLogin, FinalizeLinkError }; use termion::{ event::{Event, Key}, @@ -152,16 +152,43 @@ fn main() { error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); println!( "Just in case, here is the account info. Save it somewhere just in case!\n{:?}", - manifest.accounts.last().as_ref().unwrap() + manifest.accounts.last().unwrap().lock().unwrap() ); return; } } + let mut account = manifest + .accounts + .last() + .as_ref() + .unwrap() + .clone() + .lock() + .unwrap(); + debug!("attempting link finalization"); print!("Enter SMS code: "); let sms_code = prompt(); - linker.finalize(sms_code); + let mut tries = 0; + loop { + match linker.finalize(&mut account, sms_code) { + Ok(_) => break, + Err(FinalizeLinkError::WantMore) => { + debug!("steam wants more 2fa codes (tries: {})", tries); + tries += 1; + if tries >= 30 { + error!("Failed to finalize: unable to generate valid 2fa codes"); + break; + } + continue; + }, + Err(err) => { + error!("Failed to finalize: {}", err); + break; + } + } + } return; } diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index c285fcb..8bb1061 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -2,6 +2,7 @@ use crate::{ steamapi::{AddAuthenticatorResponse, Session, SteamApiClient}, SteamGuardAccount, }; +use log::*; use std::error::Error; use std::fmt::Display; @@ -29,30 +30,28 @@ impl AccountLinker { }; } - pub fn link(&mut self) -> anyhow::Result { + pub fn link(&mut self) -> anyhow::Result { + ensure!(!self.finalized); + let has_phone = self.client.has_phone()?; if has_phone && !self.phone_number.is_empty() { - return Err(AccountLinkError::MustRemovePhoneNumber); + bail!(AccountLinkError::MustRemovePhoneNumber); } if !has_phone && self.phone_number.is_empty() { - return Err(AccountLinkError::MustProvidePhoneNumber); + bail!(AccountLinkError::MustProvidePhoneNumber); } if !has_phone { if self.sent_confirmation_email { if !self.client.check_email_confirmation()? { - return Err(AccountLinkError::Unknown(anyhow!( - "Failed email confirmation check" - ))); + bail!("Failed email confirmation check"); } } else if !self.client.add_phone_number(self.phone_number.clone())? { - return Err(AccountLinkError::Unknown(anyhow!( - "Failed to add phone number" - ))); + bail!("Failed to add phone number"); } else { self.sent_confirmation_email = true; - return Err(AccountLinkError::MustConfirmEmail); + bail!(AccountLinkError::MustConfirmEmail); } } @@ -61,7 +60,7 @@ impl AccountLinker { match resp.status { 29 => { - return Err(AccountLinkError::AuthenticatorPresent); + bail!(AccountLinkError::AuthenticatorPresent); } 1 => { let mut account = resp.to_steam_guard_account(); @@ -70,15 +69,46 @@ impl AccountLinker { return Ok(account); } status => { - return Err(AccountLinkError::Unknown(anyhow!( - "Unknown add authenticator status code: {}", - status - ))); + bail!("Unknown add authenticator status code: {}", status); } } } - pub fn finalize(&self, account: &SteamGuardAccount, sms_code: String) {} + /// You may have to call this multiple times. If you have to call it a bunch of times, then you can assume that you are unable to generate correct 2fa codes. + pub fn finalize( + &mut self, + account: &mut SteamGuardAccount, + sms_code: String, + ) -> anyhow::Result<()> { + ensure!(!account.fully_enrolled); + ensure!(!self.finalized); + + let time = crate::steamapi::get_server_time(); + let code = account.generate_code(time); + let resp = self + .client + .finalize_authenticator(sms_code.clone(), code, time)?; + info!("finalize response status: {}", resp.status); + + match resp.status { + 89 => { + bail!(FinalizeLinkError::BadSmsCode); + } + _ => {} + } + + if !resp.success { + bail!("Failed to finalize authenticator. Status: {}", resp.status); + } + + if resp.want_more { + bail!(FinalizeLinkError::WantMore); + } + + self.finalized = true; + account.fully_enrolled = true; + return Ok(()); + } } fn generate_device_id() -> String { @@ -96,8 +126,6 @@ pub enum AccountLinkError { /// Must provide an SMS code AwaitingFinalization, AuthenticatorPresent, - NetworkFailure(reqwest::Error), - Unknown(anyhow::Error), } impl Display for AccountLinkError { @@ -108,14 +136,17 @@ impl Display for AccountLinkError { impl Error for AccountLinkError {} -impl From for AccountLinkError { - fn from(err: reqwest::Error) -> AccountLinkError { - AccountLinkError::NetworkFailure(err) +#[derive(Debug)] +pub enum FinalizeLinkError { + BadSmsCode, + /// Steam wants more 2fa codes to verify that we can generate valid codes. Call finalize again. + WantMore, +} + +impl Display for FinalizeLinkError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "{:?}", self) } } -impl From for AccountLinkError { - fn from(err: anyhow::Error) -> AccountLinkError { - AccountLinkError::Unknown(err) - } -} +impl Error for FinalizeLinkError {} diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 62bbe8a..0e6f297 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -118,7 +118,7 @@ pub fn get_server_time() -> i64 { .unwrap(); } -/// Provides raw access to the Steam API. Handles cookies, some deserialization, etc. to make it easier. +/// Provides raw access to the Steam API. Handles cookies, some deserialization, etc. to make it easier. It covers `ITwoFactorService` from the Steam web API, and some mobile app specific api endpoints. #[derive(Debug)] pub struct SteamApiClient { cookies: reqwest::cookie::Jar, @@ -382,7 +382,7 @@ impl SteamApiClient { &self, sms_code: String, code_2fa: String, - time_2fa: u64, + time_2fa: i64, ) -> anyhow::Result { ensure!(matches!(self.session, Some(_))); let params = hashmap! { @@ -447,7 +447,7 @@ fn test_login_response_parse() { #[derive(Debug, Clone, Deserialize)] pub struct SteamApiResponse { - pub response: T + pub response: T, } #[derive(Debug, Clone, Deserialize)] @@ -494,4 +494,9 @@ impl AddAuthenticatorResponse { } #[derive(Debug, Clone, Deserialize)] -pub struct FinalizeAddAuthenticatorResponse {} +pub struct FinalizeAddAuthenticatorResponse { + pub status: i32, + pub server_time: u64, + pub want_more: bool, + pub success: bool, +} From 298d29dc077280d3ccf4dc5822b462c31b519abf Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 18:44:42 -0400 Subject: [PATCH 08/24] improve error handling in accountlinker --- Cargo.lock | 21 +++++++++++ src/main.rs | 8 ++-- steamguard/Cargo.toml | 1 + steamguard/src/accountlinker.rs | 67 +++++++++++++++------------------ steamguard/src/lib.rs | 3 +- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab1ad08..f109ac8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1625,6 +1625,7 @@ dependencies = [ "serde", "serde_json", "standback", + "thiserror", "uuid", ] @@ -1781,6 +1782,26 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "0.3.4" diff --git a/src/main.rs b/src/main.rs index aba5c58..6e1d17d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,8 @@ use std::{ sync::{Arc, Mutex}, }; use steamguard::{ - steamapi, AccountLinker, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, - UserLogin, FinalizeLinkError + steamapi, AccountLinker, Confirmation, ConfirmationType, FinalizeLinkError, LoginError, + SteamGuardAccount, UserLogin, }; use termion::{ event::{Event, Key}, @@ -172,7 +172,7 @@ fn main() { let sms_code = prompt(); let mut tries = 0; loop { - match linker.finalize(&mut account, sms_code) { + match linker.finalize(&mut account, sms_code.clone()) { Ok(_) => break, Err(FinalizeLinkError::WantMore) => { debug!("steam wants more 2fa codes (tries: {})", tries); @@ -182,7 +182,7 @@ fn main() { break; } continue; - }, + } Err(err) => { error!("Failed to finalize: {}", err); break; diff --git a/steamguard/Cargo.toml b/steamguard/Cargo.toml index 466dbfe..f7e91d6 100644 --- a/steamguard/Cargo.toml +++ b/steamguard/Cargo.toml @@ -23,3 +23,4 @@ uuid = { version = "0.8", features = ["v4"] } log = "0.4.14" scraper = "0.12.0" maplit = "1.0.2" +thiserror = "1.0.26" diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 8bb1061..69174d4 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,9 +1,9 @@ use crate::{ - steamapi::{AddAuthenticatorResponse, Session, SteamApiClient}, + steamapi::{AddAuthenticatorResponse, Session, SteamApiClient, FinalizeAddAuthenticatorResponse}, SteamGuardAccount, }; use log::*; -use std::error::Error; +use thiserror::Error; use std::fmt::Display; #[derive(Debug)] @@ -30,28 +30,27 @@ impl AccountLinker { }; } - pub fn link(&mut self) -> anyhow::Result { - ensure!(!self.finalized); + pub fn link(&mut self) -> anyhow::Result { let has_phone = self.client.has_phone()?; if has_phone && !self.phone_number.is_empty() { - bail!(AccountLinkError::MustRemovePhoneNumber); + return Err(AccountLinkError::MustRemovePhoneNumber); } if !has_phone && self.phone_number.is_empty() { - bail!(AccountLinkError::MustProvidePhoneNumber); + return Err(AccountLinkError::MustProvidePhoneNumber); } if !has_phone { if self.sent_confirmation_email { if !self.client.check_email_confirmation()? { - bail!("Failed email confirmation check"); + return Err(anyhow!("Failed email confirmation check"))?; } } else if !self.client.add_phone_number(self.phone_number.clone())? { - bail!("Failed to add phone number"); + return Err(anyhow!("Failed to add phone number"))?; } else { self.sent_confirmation_email = true; - bail!(AccountLinkError::MustConfirmEmail); + return Err(AccountLinkError::MustConfirmEmail); } } @@ -60,7 +59,7 @@ impl AccountLinker { match resp.status { 29 => { - bail!(AccountLinkError::AuthenticatorPresent); + return Err(AccountLinkError::AuthenticatorPresent); } 1 => { let mut account = resp.to_steam_guard_account(); @@ -69,7 +68,7 @@ impl AccountLinker { return Ok(account); } status => { - bail!("Unknown add authenticator status code: {}", status); + return Err(anyhow!("Unknown add authenticator status code: {}", status))?; } } } @@ -79,30 +78,27 @@ impl AccountLinker { &mut self, account: &mut SteamGuardAccount, sms_code: String, - ) -> anyhow::Result<()> { - ensure!(!account.fully_enrolled); - ensure!(!self.finalized); - + ) -> anyhow::Result<(), FinalizeLinkError> { let time = crate::steamapi::get_server_time(); let code = account.generate_code(time); - let resp = self + let resp: FinalizeAddAuthenticatorResponse = self .client .finalize_authenticator(sms_code.clone(), code, time)?; info!("finalize response status: {}", resp.status); match resp.status { 89 => { - bail!(FinalizeLinkError::BadSmsCode); + return Err(FinalizeLinkError::BadSmsCode); } _ => {} } if !resp.success { - bail!("Failed to finalize authenticator. Status: {}", resp.status); + return Err(FinalizeLinkError::Failure { status: resp.status })?; } if resp.want_more { - bail!(FinalizeLinkError::WantMore); + return Err(FinalizeLinkError::WantMore); } self.finalized = true; @@ -115,38 +111,35 @@ fn generate_device_id() -> String { return format!("android:{}", uuid::Uuid::new_v4().to_string()); } -#[derive(Debug)] +#[derive(Error, Debug)] pub enum AccountLinkError { /// No phone number on the account + #[error("A phone number is needed, but not already present on the account.")] MustProvidePhoneNumber, /// A phone number is already on the account + #[error("A phone number was provided, but one is already present on the account.")] MustRemovePhoneNumber, /// User need to click link from confirmation email + #[error("An email has been sent to the user's email, click the link in that email.")] MustConfirmEmail, /// Must provide an SMS code + #[error("Awaiting finalization")] AwaitingFinalization, + #[error("Authenticator is already present.")] AuthenticatorPresent, + #[error(transparent)] + Unknown(#[from] anyhow::Error), } -impl Display for AccountLinkError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - write!(f, "{:?}", self) - } -} - -impl Error for AccountLinkError {} - -#[derive(Debug)] +#[derive(Error, Debug)] pub enum FinalizeLinkError { + #[error("Provided SMS code was incorrect.")] BadSmsCode, /// Steam wants more 2fa codes to verify that we can generate valid codes. Call finalize again. + #[error("Steam wants more 2fa codes for verification.")] WantMore, + #[error("Finalization was not successful. Status code {status:?}")] + Failure{ status: i32 }, + #[error(transparent)] + Unknown(#[from] anyhow::Error), } - -impl Display for FinalizeLinkError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - write!(f, "{:?}", self) - } -} - -impl Error for FinalizeLinkError {} diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index 8ea4f83..b240f3e 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -1,4 +1,4 @@ -pub use accountlinker::{AccountLinkError, AccountLinker}; +pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError}; use anyhow::Result; pub use confirmation::{Confirmation, ConfirmationType}; use hmacsha1::hmac_sha1; @@ -11,7 +11,6 @@ use reqwest::{ use scraper::{Html, Selector}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, convert::TryInto}; -pub use steamapi::AddAuthenticatorResponse; pub use userlogin::{LoginError, UserLogin}; #[macro_use] extern crate lazy_static; From 467e669fb82cb05e030f16d8a445282845a9aa60 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 19:09:48 -0400 Subject: [PATCH 09/24] actually handle account link errors --- src/main.rs | 28 +++++++++++++++++++++++++--- steamguard/src/accountlinker.rs | 23 +++++++++++------------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6e1d17d..30b7991 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,8 @@ use std::{ sync::{Arc, Mutex}, }; use steamguard::{ - steamapi, AccountLinker, Confirmation, ConfirmationType, FinalizeLinkError, LoginError, - SteamGuardAccount, UserLogin, + steamapi, AccountLinkError, AccountLinker, Confirmation, ConfirmationType, FinalizeLinkError, + LoginError, SteamGuardAccount, UserLogin, }; use termion::{ event::{Event, Key}, @@ -136,6 +136,22 @@ fn main() { account = a; break; } + Err(AccountLinkError::MustRemovePhoneNumber) => { + println!("There is already a phone number on this account, please remove it and try again."); + return; + } + Err(AccountLinkError::MustProvidePhoneNumber) => { + print!("Enter your phone number:"); + linker.phone_number = prompt(); + } + Err(AccountLinkError::AuthenticatorPresent) => { + println!("An authenticator is already present on this account."); + return; + } + Err(AccountLinkError::MustConfirmEmail) => { + println!("Check your email and click the link."); + pause(); + } Err(err) => { error!( "Failed to link authenticator. Account has not been linked. {}", @@ -510,7 +526,6 @@ fn do_login_raw() -> anyhow::Result { } Err(LoginError::Need2FA) => { print!("Enter 2fa code: "); - let server_time = steamapi::get_server_time(); login.twofactor_code = prompt(); } Err(LoginError::NeedCaptcha { captcha_gid }) => { @@ -535,6 +550,13 @@ fn do_login_raw() -> anyhow::Result { } } +fn pause() { + println!("Press any key to continue..."); + let mut stdout = stdout().into_raw_mode().unwrap(); + stdout.flush().unwrap(); + stdin().events().next(); +} + fn demo_confirmation_menu() { info!("showing demo menu"); let (accept, deny) = prompt_confirmation_menu(vec![ diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 69174d4..e8a3243 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,15 +1,16 @@ use crate::{ - steamapi::{AddAuthenticatorResponse, Session, SteamApiClient, FinalizeAddAuthenticatorResponse}, + steamapi::{ + AddAuthenticatorResponse, FinalizeAddAuthenticatorResponse, Session, SteamApiClient, + }, SteamGuardAccount, }; use log::*; use thiserror::Error; -use std::fmt::Display; #[derive(Debug)] pub struct AccountLinker { device_id: String, - phone_number: String, + pub phone_number: String, pub account: Option, pub finalized: bool, sent_confirmation_email: bool, @@ -31,7 +32,6 @@ impl AccountLinker { } pub fn link(&mut self) -> anyhow::Result { - let has_phone = self.client.has_phone()?; if has_phone && !self.phone_number.is_empty() { @@ -81,9 +81,9 @@ impl AccountLinker { ) -> anyhow::Result<(), FinalizeLinkError> { let time = crate::steamapi::get_server_time(); let code = account.generate_code(time); - let resp: FinalizeAddAuthenticatorResponse = self - .client - .finalize_authenticator(sms_code.clone(), code, time)?; + let resp: FinalizeAddAuthenticatorResponse = + self.client + .finalize_authenticator(sms_code.clone(), code, time)?; info!("finalize response status: {}", resp.status); match resp.status { @@ -94,7 +94,9 @@ impl AccountLinker { } if !resp.success { - return Err(FinalizeLinkError::Failure { status: resp.status })?; + return Err(FinalizeLinkError::Failure { + status: resp.status, + })?; } if resp.want_more { @@ -122,9 +124,6 @@ pub enum AccountLinkError { /// User need to click link from confirmation email #[error("An email has been sent to the user's email, click the link in that email.")] MustConfirmEmail, - /// Must provide an SMS code - #[error("Awaiting finalization")] - AwaitingFinalization, #[error("Authenticator is already present.")] AuthenticatorPresent, #[error(transparent)] @@ -139,7 +138,7 @@ pub enum FinalizeLinkError { #[error("Steam wants more 2fa codes for verification.")] WantMore, #[error("Finalization was not successful. Status code {status:?}")] - Failure{ status: i32 }, + Failure { status: i32 }, #[error(transparent)] Unknown(#[from] anyhow::Error), } From 071d9d7d2df67c0bbed3deb111c052dc985208d5 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 19:48:18 -0400 Subject: [PATCH 10/24] enable logging for steamguard module --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 30b7991..d4bd85f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,6 +98,7 @@ fn main() { stderrlog::new() .verbosity(verbosity) .module(module_path!()) + .module("steamguard") .init() .unwrap(); From afafe44d609a7a9d2f8be2b50e105ff4e739fafe Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 19:49:53 -0400 Subject: [PATCH 11/24] sometimes webcookie field can be missing? I got a login response that did not include the `webcookie` field. Added a test case for it. --- .../login-response-missing-webcookie.json | 1 + steamguard/src/steamapi.rs | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 steamguard/src/fixtures/api-responses/login-response-missing-webcookie.json diff --git a/steamguard/src/fixtures/api-responses/login-response-missing-webcookie.json b/steamguard/src/fixtures/api-responses/login-response-missing-webcookie.json new file mode 100644 index 0000000..94e933d --- /dev/null +++ b/steamguard/src/fixtures/api-responses/login-response-missing-webcookie.json @@ -0,0 +1 @@ +{"success":true,"requires_twofactor":false,"redirect_uri":"steammobile:\/\/mobileloginsucceeded","login_complete":true,"oauth":"{\"steamid\":\"92591609556178617\",\"account_name\":\"hydrastar2\",\"oauth_token\":\"1cc83205dab2979e558534dab29f6f3aa\",\"wgtoken\":\"3EDA9DEF07D7B39361D95203525D8AFE82A\",\"wgtoken_secure\":\"F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48\"}"} \ No newline at end of file diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 0e6f297..32a86a8 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -86,6 +86,7 @@ pub struct OAuthData { steamid: String, wgtoken: String, wgtoken_secure: String, + #[serde(default)] webcookie: String, } @@ -97,7 +98,7 @@ pub struct Session { pub steam_login: String, #[serde(rename = "SteamLoginSecure")] pub steam_login_secure: String, - #[serde(rename = "WebCookie")] + #[serde(default, rename = "WebCookie")] pub web_cookie: String, #[serde(rename = "OAuthToken")] pub token: String, @@ -445,6 +446,30 @@ fn test_login_response_parse() { assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5"); } +#[test] +fn test_login_response_parse_missing_webcookie() { + let result = serde_json::from_str::(include_str!( + "fixtures/api-responses/login-response-missing-webcookie.json" + )); + + assert!( + matches!(result, Ok(_)), + "got error: {}", + result.unwrap_err() + ); + let resp = result.unwrap(); + + let oauth = resp.oauth.unwrap(); + assert_eq!(oauth.steamid, "92591609556178617"); + assert_eq!(oauth.oauth_token, "1cc83205dab2979e558534dab29f6f3aa"); + assert_eq!(oauth.wgtoken, "3EDA9DEF07D7B39361D95203525D8AFE82A"); + assert_eq!( + oauth.wgtoken_secure, + "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48" + ); + assert_eq!(oauth.webcookie, ""); +} + #[derive(Debug, Clone, Deserialize)] pub struct SteamApiResponse { pub response: T, From b2414e0c3322aa38c2b787e830853fdc244bc911 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 20:05:23 -0400 Subject: [PATCH 12/24] fixed session not being transfered to AccountLinker --- steamguard/src/accountlinker.rs | 4 ++-- steamguard/src/steamapi.rs | 19 +++++++++++-------- steamguard/src/userlogin.rs | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index e8a3243..e344f22 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -26,8 +26,8 @@ impl AccountLinker { account: None, finalized: false, sent_confirmation_email: false, - session: session, - client: SteamApiClient::new(), + session: session.clone(), + client: SteamApiClient::new(Some(session)), }; } diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 32a86a8..14d7c33 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -128,7 +128,7 @@ pub struct SteamApiClient { } impl SteamApiClient { - pub fn new() -> SteamApiClient { + pub fn new(session: Option) -> SteamApiClient { SteamApiClient { cookies: reqwest::cookie::Jar::default(), client: reqwest::blocking::ClientBuilder::new() @@ -139,17 +139,18 @@ impl SteamApiClient { }.into_iter())) .build() .unwrap(), - session: None, + session: session, } } fn build_session(&self, data: &OAuthData) -> Session { + trace!("SteamApiClient::build_session"); return Session { token: data.oauth_token.clone(), steam_id: data.steamid.parse().unwrap(), steam_login: format!("{}%7C%7C{}", data.steamid, data.wgtoken), steam_login_secure: format!("{}%7C%7C{}", data.steamid, data.wgtoken_secure), - session_id: self.extract_session_id().unwrap(), + session_id: self.extract_session_id().expect("failed to extract session id from cookies"), web_cookie: data.webcookie.clone(), }; } @@ -249,6 +250,7 @@ impl SteamApiClient { .post("https://steamcommunity.com/login/dologin") .form(¶ms) .send()?; + self.save_cookies_from_response(&resp); let text = resp.text()?; trace!("raw login response: {}", text); @@ -351,7 +353,7 @@ impl SteamApiClient { /// /// Host: api.steampowered.com /// Endpoint: POST /ITwoFactorService/AddAuthenticator/v0001 - pub fn add_authenticator(&self, device_id: String) -> anyhow::Result { + pub fn add_authenticator(&mut self, device_id: String) -> anyhow::Result { ensure!(matches!(self.session, Some(_))); let params = hashmap! { "access_token" => self.session.as_ref().unwrap().token.clone(), @@ -361,15 +363,16 @@ impl SteamApiClient { "sms_phone_id" => "1".into(), }; - let text = self + let resp = self .post(format!( "{}/ITwoFactorService/AddAuthenticator/v0001", STEAM_API_BASE.to_string() )) .form(¶ms) - .send()? - .text()?; - trace!("raw login response: {}", text); + .send()?; + self.save_cookies_from_response(&resp); + let text = resp.text()?; + trace!("raw add authenticator response: {}", text); let resp: SteamApiResponse = serde_json::from_str(text.as_str())?; diff --git a/steamguard/src/userlogin.rs b/steamguard/src/userlogin.rs index 351bfe3..6c60703 100644 --- a/steamguard/src/userlogin.rs +++ b/steamguard/src/userlogin.rs @@ -61,7 +61,7 @@ impl UserLogin { twofactor_code: String::from(""), email_code: String::from(""), steam_id: 0, - client: SteamApiClient::new(), + client: SteamApiClient::new(None), }; } From bb762b2f47ff7e6d2a0af27a84c0be61cb4b4203 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 20:11:15 -0400 Subject: [PATCH 13/24] add some more logging in phoneajax --- src/main.rs | 2 +- steamguard/src/steamapi.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index d4bd85f..f07fffb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,7 +142,7 @@ fn main() { return; } Err(AccountLinkError::MustProvidePhoneNumber) => { - print!("Enter your phone number:"); + print!("Enter your phone number: "); linker.phone_number = prompt(); } Err(AccountLinkError::AuthenticatorPresent) => { diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 14d7c33..9b3a894 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -315,18 +315,19 @@ impl SteamApiClient { .send()?; let result: Value = resp.json()?; + trace!("phoneajax: {:?}", result); if result["has_phone"] != Value::Null { - trace!("found has_phone field"); + trace!("op: {} - found has_phone field", op); return result["has_phone"] .as_bool() .ok_or(anyhow!("failed to parse has_phone field into boolean")); } else if result["success"] != Value::Null { - trace!("found success field"); + trace!("op: {} - found success field", op); return result["success"] .as_bool() .ok_or(anyhow!("failed to parse success field into boolean")); } else { - trace!("did not find any expected field"); + trace!("op: {} - did not find any expected field", op); return Ok(false); } } From c17bdf92a593180fbf475e930f511aae7bcab601 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 20:40:06 -0400 Subject: [PATCH 14/24] apply session id cookie if session is present to all requests --- steamguard/src/steamapi.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 9b3a894..6306265 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -179,24 +179,28 @@ impl SteamApiClient { } } - pub fn request(&self, method: reqwest::Method, url: U) -> RequestBuilder { + pub fn request(&self, method: reqwest::Method, url: U) -> RequestBuilder { + trace!("making request: {} {}", method, url); self.cookies .add_cookie_str("mobileClientVersion=0 (2.1.3)", &STEAM_COOKIE_URL); self.cookies .add_cookie_str("mobileClient=android", &STEAM_COOKIE_URL); self.cookies .add_cookie_str("Steam_Language=english", &STEAM_COOKIE_URL); + if let Some(session) = &self.session { + self.cookies.add_cookie_str(format!("sessionid={}", session.session_id).as_str(), &STEAM_COOKIE_URL); + } self.client .request(method, url) .header(COOKIE, self.cookies.cookies(&STEAM_COOKIE_URL).unwrap()) } - pub fn get(&self, url: U) -> RequestBuilder { + pub fn get(&self, url: U) -> RequestBuilder { self.request(reqwest::Method::GET, url) } - pub fn post(&self, url: U) -> RequestBuilder { + pub fn post(&self, url: U) -> RequestBuilder { self.request(reqwest::Method::POST, url) } @@ -298,6 +302,7 @@ impl SteamApiClient { } /// Endpoint: POST /steamguard/phoneajax + /// Requires `sessionid` cookie to be set. fn phoneajax(&self, op: &str, arg: &str) -> anyhow::Result { let mut params = hashmap! { "op" => op, From 691d9270508326bb67c2acb822507add49fa969d Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 21:41:20 -0400 Subject: [PATCH 15/24] adding phone numbers does not work --- src/main.rs | 5 +++-- steamguard/src/accountlinker.rs | 38 ++++++++++++++++----------------- steamguard/src/steamapi.rs | 20 ++++++++++++++++- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index f07fffb..eb6cb38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,8 +142,9 @@ fn main() { return; } Err(AccountLinkError::MustProvidePhoneNumber) => { - print!("Enter your phone number: "); - linker.phone_number = prompt(); + println!("Enter your phone number in the following format: +1 123-456-7890"); + print!("Phone number: "); + linker.phone_number = prompt().replace(&['(', ')', '-'][..], ""); } Err(AccountLinkError::AuthenticatorPresent) => { println!("An authenticator is already present on this account."); diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index e344f22..d45e569 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -32,27 +32,27 @@ impl AccountLinker { } pub fn link(&mut self) -> anyhow::Result { - let has_phone = self.client.has_phone()?; + // let has_phone = self.client.has_phone()?; - if has_phone && !self.phone_number.is_empty() { - return Err(AccountLinkError::MustRemovePhoneNumber); - } - if !has_phone && self.phone_number.is_empty() { - return Err(AccountLinkError::MustProvidePhoneNumber); - } + // if has_phone && !self.phone_number.is_empty() { + // return Err(AccountLinkError::MustRemovePhoneNumber); + // } + // if !has_phone && self.phone_number.is_empty() { + // return Err(AccountLinkError::MustProvidePhoneNumber); + // } - if !has_phone { - if self.sent_confirmation_email { - if !self.client.check_email_confirmation()? { - return Err(anyhow!("Failed email confirmation check"))?; - } - } else if !self.client.add_phone_number(self.phone_number.clone())? { - return Err(anyhow!("Failed to add phone number"))?; - } else { - self.sent_confirmation_email = true; - return Err(AccountLinkError::MustConfirmEmail); - } - } + // if !has_phone { + // if self.sent_confirmation_email { + // if !self.client.check_email_confirmation()? { + // return Err(anyhow!("Failed email confirmation check"))?; + // } + // } else if !self.client.add_phone_number(self.phone_number.clone())? { + // return Err(anyhow!("Failed to add phone number"))?; + // } else { + // self.sent_confirmation_email = true; + // return Err(AccountLinkError::MustConfirmEmail); + // } + // } let resp: AddAuthenticatorResponse = self.client.add_authenticator(self.device_id.clone())?; diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 6306265..919f6a2 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -301,6 +301,9 @@ impl SteamApiClient { } } + /// One of the endpoints that handles phone number things. Can check to see if phone is present on account, and maybe do some other stuff. It's not really super clear. + /// + /// Host: steamcommunity.com /// Endpoint: POST /steamguard/phoneajax /// Requires `sessionid` cookie to be set. fn phoneajax(&self, op: &str, arg: &str) -> anyhow::Result { @@ -319,6 +322,7 @@ impl SteamApiClient { .form(¶ms) .send()?; + trace!("phoneajax: status={}", resp.status()); let result: Value = resp.json()?; trace!("phoneajax: {:?}", result); if result["has_phone"] != Value::Null { @@ -350,7 +354,21 @@ impl SteamApiClient { } pub fn add_phone_number(&self, phone_number: String) -> anyhow::Result { - return self.phoneajax("add_phone_number", phone_number.as_str()); + // return self.phoneajax("add_phone_number", phone_number.as_str()); + todo!(); + } + + /// Provides lots of juicy information, like if the number is a VOIP number. + /// Host: store.steampowered.com + /// Endpoint: POST /phone/validate + /// Found on page: https://store.steampowered.com/phone/add + pub fn phone_validate(&self, phone_number: String) -> anyhow::Result { + let params = hashmap!{ + "sessionID" => "", + "phoneNumber" => "", + }; + + todo!(); } /// Starts the authenticator linking process. From ad2cdd2a7e1879aa4d0330b828e4bb342f31c630 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 22:00:49 -0400 Subject: [PATCH 16/24] fix parsing for add authenticator response --- .../api-responses/add-authenticator-1.json | 1 + steamguard/src/steamapi.rs | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 steamguard/src/fixtures/api-responses/add-authenticator-1.json diff --git a/steamguard/src/fixtures/api-responses/add-authenticator-1.json b/steamguard/src/fixtures/api-responses/add-authenticator-1.json new file mode 100644 index 0000000..536f196 --- /dev/null +++ b/steamguard/src/fixtures/api-responses/add-authenticator-1.json @@ -0,0 +1 @@ +{"response":{"shared_secret":"wGwZx=sX5MmTxi6QgA3Gi","serial_number":"72016503753671","revocation_code":"R123456","uri":"otpauth://totp/Steam:hydrastar2?secret=JRX7DZIF4JNA3QE3UMS4BDACDISZTRWA&issuer=Steam","server_time":"1628559846","account_name":"hydrastar2","token_gid":"fe12390348285d7f4","identity_secret":"soo58ouTUV+5=KhRKDVK","secret_1":"Me7ngFQsY9R=x3EQyOU","status":1}} \ No newline at end of file diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 919f6a2..eff5460 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -513,6 +513,7 @@ pub struct AddAuthenticatorResponse { /// URI for QR code generation pub uri: String, /// Current server time + #[serde(deserialize_with = "parse_json_string_as_number")] pub server_time: u64, /// Account name to display on token client pub account_name: String, @@ -552,3 +553,30 @@ pub struct FinalizeAddAuthenticatorResponse { pub want_more: bool, pub success: bool, } + +fn parse_json_string_as_number<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + // for some reason, deserializing to &str doesn't work but this does. + let s: String = Deserialize::deserialize(deserializer)?; + Ok(s.parse().unwrap()) +} + +#[test] +fn test_parse_add_auth_response() { + let result = serde_json::from_str::>(include_str!( + "fixtures/api-responses/add-authenticator-1.json" + )); + + assert!( + matches!(result, Ok(_)), + "got error: {}", + result.unwrap_err() + ); + let resp = result.unwrap().response; + + assert_eq!(resp.server_time, 1628559846); + assert_eq!(resp.shared_secret, "wGwZx=sX5MmTxi6QgA3Gi"); + assert_eq!(resp.revocation_code, "R123456"); +} \ No newline at end of file From 5cfdb84bc290bb0dbd26a20a04ba869b1d6ecaa6 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 22:11:09 -0400 Subject: [PATCH 17/24] add trace logging for finalize_authenticator --- steamguard/src/steamapi.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index eff5460..bb9661e 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -421,14 +421,18 @@ impl SteamApiClient { "authenticator_time" => time_2fa.to_string(), }; - let resp: SteamApiResponse = self + let resp = self .post(format!( "{}/ITwoFactorService/FinalizeAddAuthenticator/v0001", STEAM_API_BASE.to_string() )) .form(¶ms) - .send()? - .json()?; + .send()?; + + let text = resp.text()?; + trace!("raw finalize authenticator response: {}", text); + + let resp: SteamApiResponse = serde_json::from_str(text.as_str())?; return Ok(resp.response); } @@ -549,6 +553,7 @@ impl AddAuthenticatorResponse { #[derive(Debug, Clone, Deserialize)] pub struct FinalizeAddAuthenticatorResponse { pub status: i32, + #[serde(deserialize_with = "parse_json_string_as_number")] pub server_time: u64, pub want_more: bool, pub success: bool, From 91e2a1b2c68935b3b2b7b2a2f0fadcb27838e0e3 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 22:25:39 -0400 Subject: [PATCH 18/24] fix username arg not accepting values --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index eb6cb38..dbc5f59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ fn main() { Arg::with_name("username") .long("username") .short("u") + .takes_value(true) .help("Select the account you want by steam username. By default, the first account in the manifest is selected.") ) .arg( From 555b47b3fbf8eb0e74617e01c690c6ecae544afc Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 22:46:50 -0400 Subject: [PATCH 19/24] fix add authenticator response parsing again --- .../api-responses/add-authenticator-2.json | 1 + steamguard/src/steamapi.rs | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 steamguard/src/fixtures/api-responses/add-authenticator-2.json diff --git a/steamguard/src/fixtures/api-responses/add-authenticator-2.json b/steamguard/src/fixtures/api-responses/add-authenticator-2.json new file mode 100644 index 0000000..55bac0a --- /dev/null +++ b/steamguard/src/fixtures/api-responses/add-authenticator-2.json @@ -0,0 +1 @@ +{"response":{"status":29}} \ No newline at end of file diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index bb9661e..bb92665 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -509,23 +509,31 @@ pub struct SteamApiResponse { #[derive(Debug, Clone, Deserialize)] pub struct AddAuthenticatorResponse { /// Shared secret between server and authenticator + #[serde(default)] pub shared_secret: String, /// Authenticator serial number (unique per token) + #[serde(default)] pub serial_number: String, /// code used to revoke authenticator + #[serde(default)] pub revocation_code: String, /// URI for QR code generation + #[serde(default)] pub uri: String, /// Current server time - #[serde(deserialize_with = "parse_json_string_as_number")] + #[serde(default, deserialize_with = "parse_json_string_as_number")] pub server_time: u64, /// Account name to display on token client + #[serde(default)] pub account_name: String, /// Token GID assigned by server + #[serde(default)] pub token_gid: String, /// Secret used for identity attestation (e.g., for eventing) + #[serde(default)] pub identity_secret: String, /// Spare shared secret + #[serde(default)] pub secret_1: String, /// Result code pub status: i32, @@ -584,4 +592,20 @@ fn test_parse_add_auth_response() { assert_eq!(resp.server_time, 1628559846); assert_eq!(resp.shared_secret, "wGwZx=sX5MmTxi6QgA3Gi"); assert_eq!(resp.revocation_code, "R123456"); -} \ No newline at end of file +} + +#[test] +fn test_parse_add_auth_response2() { + let result = serde_json::from_str::>(include_str!( + "fixtures/api-responses/add-authenticator-2.json" + )); + + assert!( + matches!(result, Ok(_)), + "got error: {}", + result.unwrap_err() + ); + let resp = result.unwrap().response; + + assert_eq!(resp.status, 29); +} From c3d3f33ec35d9182ece285771071a6df86e12125 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 23:08:51 -0400 Subject: [PATCH 20/24] add remove_authenticator --- steamguard/src/steamapi.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index bb92665..19d054e 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -436,6 +436,32 @@ impl SteamApiClient { return Ok(resp.response); } + + /// Host: api.steampowered.com + /// Endpoint: POST /ITwoFactorService/RemoveAuthenticator/v0001 + pub fn remove_authenticator(&self, revocation_code: String) -> anyhow::Result { + let params = hashmap!{ + "steamid" => self.session.as_ref().unwrap().steam_id.to_string(), + "steamguard_scheme" => "2".into(), + "revocation_code" => revocation_code, + "access_token" => self.session.as_ref().unwrap().token.to_string(), + }; + + let resp = self + .post(format!( + "{}/ITwoFactorService/RemoveAuthenticator/v0001", + STEAM_API_BASE.to_string() + )) + .form(¶ms) + .send()?; + + let text = resp.text()?; + trace!("raw remove authenticator response: {}", text); + + let resp: SteamApiResponse = serde_json::from_str(text.as_str())?; + + return Ok(resp.response); + } } #[test] @@ -567,6 +593,11 @@ pub struct FinalizeAddAuthenticatorResponse { pub success: bool, } +#[derive(Debug, Clone, Deserialize)] +pub struct RemoveAuthenticatorResponse { + pub success: bool +} + fn parse_json_string_as_number<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, From 72c0acfbf1c94f960158cefa37afea0131b259f4 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 10 Aug 2021 17:17:41 -0400 Subject: [PATCH 21/24] cargo fmt --- steamguard/src/steamapi.rs | 42 +++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 19d054e..a353ead 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -150,7 +150,9 @@ impl SteamApiClient { steam_id: data.steamid.parse().unwrap(), steam_login: format!("{}%7C%7C{}", data.steamid, data.wgtoken), steam_login_secure: format!("{}%7C%7C{}", data.steamid, data.wgtoken_secure), - session_id: self.extract_session_id().expect("failed to extract session id from cookies"), + session_id: self + .extract_session_id() + .expect("failed to extract session id from cookies"), web_cookie: data.webcookie.clone(), }; } @@ -179,7 +181,11 @@ impl SteamApiClient { } } - pub fn request(&self, method: reqwest::Method, url: U) -> RequestBuilder { + pub fn request( + &self, + method: reqwest::Method, + url: U, + ) -> RequestBuilder { trace!("making request: {} {}", method, url); self.cookies .add_cookie_str("mobileClientVersion=0 (2.1.3)", &STEAM_COOKIE_URL); @@ -188,7 +194,10 @@ impl SteamApiClient { self.cookies .add_cookie_str("Steam_Language=english", &STEAM_COOKIE_URL); if let Some(session) = &self.session { - self.cookies.add_cookie_str(format!("sessionid={}", session.session_id).as_str(), &STEAM_COOKIE_URL); + self.cookies.add_cookie_str( + format!("sessionid={}", session.session_id).as_str(), + &STEAM_COOKIE_URL, + ); } self.client @@ -363,7 +372,7 @@ impl SteamApiClient { /// Endpoint: POST /phone/validate /// Found on page: https://store.steampowered.com/phone/add pub fn phone_validate(&self, phone_number: String) -> anyhow::Result { - let params = hashmap!{ + let params = hashmap! { "sessionID" => "", "phoneNumber" => "", }; @@ -377,7 +386,10 @@ impl SteamApiClient { /// /// Host: api.steampowered.com /// Endpoint: POST /ITwoFactorService/AddAuthenticator/v0001 - pub fn add_authenticator(&mut self, device_id: String) -> anyhow::Result { + pub fn add_authenticator( + &mut self, + device_id: String, + ) -> anyhow::Result { ensure!(matches!(self.session, Some(_))); let params = hashmap! { "access_token" => self.session.as_ref().unwrap().token.clone(), @@ -432,15 +444,19 @@ impl SteamApiClient { let text = resp.text()?; trace!("raw finalize authenticator response: {}", text); - let resp: SteamApiResponse = serde_json::from_str(text.as_str())?; + let resp: SteamApiResponse = + serde_json::from_str(text.as_str())?; return Ok(resp.response); } /// Host: api.steampowered.com /// Endpoint: POST /ITwoFactorService/RemoveAuthenticator/v0001 - pub fn remove_authenticator(&self, revocation_code: String) -> anyhow::Result { - let params = hashmap!{ + pub fn remove_authenticator( + &self, + revocation_code: String, + ) -> anyhow::Result { + let params = hashmap! { "steamid" => self.session.as_ref().unwrap().steam_id.to_string(), "steamguard_scheme" => "2".into(), "revocation_code" => revocation_code, @@ -458,7 +474,8 @@ impl SteamApiClient { let text = resp.text()?; trace!("raw remove authenticator response: {}", text); - let resp: SteamApiResponse = serde_json::from_str(text.as_str())?; + let resp: SteamApiResponse = + serde_json::from_str(text.as_str())?; return Ok(resp.response); } @@ -520,10 +537,7 @@ fn test_login_response_parse_missing_webcookie() { assert_eq!(oauth.steamid, "92591609556178617"); assert_eq!(oauth.oauth_token, "1cc83205dab2979e558534dab29f6f3aa"); assert_eq!(oauth.wgtoken, "3EDA9DEF07D7B39361D95203525D8AFE82A"); - assert_eq!( - oauth.wgtoken_secure, - "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48" - ); + assert_eq!(oauth.wgtoken_secure, "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48"); assert_eq!(oauth.webcookie, ""); } @@ -595,7 +609,7 @@ pub struct FinalizeAddAuthenticatorResponse { #[derive(Debug, Clone, Deserialize)] pub struct RemoveAuthenticatorResponse { - pub success: bool + pub success: bool, } fn parse_json_string_as_number<'de, D>(deserializer: D) -> Result From 217a88f0deaeafe12b0a94c8beb3063bf88193bf Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 10 Aug 2021 20:28:20 -0400 Subject: [PATCH 22/24] add phone_add_ajaxop --- steamguard/src/steamapi.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index a353ead..18e4d9c 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -350,6 +350,33 @@ impl SteamApiClient { } } + /// Works similar to phoneajax. Used in the process to add a phone number to a steam account. + /// Valid ops: + /// - get_phone_number => `input` is treated as a phone number to add to the account. Yes, this is somewhat counter intuitive. + /// - resend_sms + /// - get_sms_code => `input` is treated as a the SMS code that was texted to the phone number. Again, this is somewhat counter intuitive. + /// + /// Host: store.steampowered.com + /// Endpoint: /phone/add_ajaxop + fn phone_add_ajaxop(&self, op: &str, input: &str) -> anyhow::Result<()> { + trace!("phone_add_ajaxop: op={} input={}", op, input); + let params = hashmap! { + "op" => op, + "input" => input, + "sessionid" => self.session.as_ref().unwrap().session_id.as_str(), + }; + + let resp = self + .post("https://steamcommunity.com/steamguard/phoneajax") + .form(¶ms) + .send()?; + trace!("phone_add_ajaxop: http status={}", resp.status()); + let text = resp.text()?; + trace!("phone_add_ajaxop response: {}", text); + + todo!(); + } + pub fn has_phone(&self) -> anyhow::Result { return self.phoneajax("has_phone", "null"); } @@ -542,6 +569,7 @@ fn test_login_response_parse_missing_webcookie() { } #[derive(Debug, Clone, Deserialize)] +#[serde(transparent)] pub struct SteamApiResponse { pub response: T, } From 5379e9572c70efef6c8d5de367d75178c1316d2d Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 10 Aug 2021 20:54:01 -0400 Subject: [PATCH 23/24] document phone_add_ajaxop more --- .../confirmations/multiple-confirmations.html | 2 +- .../confirmations/phone-number-change.html | 209 ++++++++++++++++++ steamguard/src/steamapi.rs | 6 +- 3 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 steamguard/src/fixtures/confirmations/phone-number-change.html diff --git a/steamguard/src/fixtures/confirmations/multiple-confirmations.html b/steamguard/src/fixtures/confirmations/multiple-confirmations.html index 23bc936..f82c84e 100644 --- a/steamguard/src/fixtures/confirmations/multiple-confirmations.html +++ b/steamguard/src/fixtures/confirmations/multiple-confirmations.html @@ -120,7 +120,7 @@ if ( typeof JSON != 'object' || !JSON.stringify || !JSON.parse ) { document.writ
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+ + + +
+ +
+
+
+
+
+
Account recovery
+
+
Just now
+
+
+ +
+
+
+ + + + + + + + +
+ + + + + +
+ +
+ + \ No newline at end of file diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 18e4d9c..d44c69a 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -354,7 +354,9 @@ impl SteamApiClient { /// Valid ops: /// - get_phone_number => `input` is treated as a phone number to add to the account. Yes, this is somewhat counter intuitive. /// - resend_sms - /// - get_sms_code => `input` is treated as a the SMS code that was texted to the phone number. Again, this is somewhat counter intuitive. + /// - get_sms_code => `input` is treated as a the SMS code that was texted to the phone number. Again, this is somewhat counter intuitive. After this succeeds, the phone number is added to the account. + /// - email_verification => If the account is protected with steam guard email, a verification link is sent. After the link in the email is clicked, send this op. After, an SMS code is sent to the phone number. + /// - retry_email_verification /// /// Host: store.steampowered.com /// Endpoint: /phone/add_ajaxop @@ -367,7 +369,7 @@ impl SteamApiClient { }; let resp = self - .post("https://steamcommunity.com/steamguard/phoneajax") + .post("https://store.steampowered.com/phone/add_ajaxop") .form(¶ms) .send()?; trace!("phone_add_ajaxop: http status={}", resp.status()); From 51fea91577b9cf47dc9039a50cbcb58d3de4019c Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 10 Aug 2021 20:54:29 -0400 Subject: [PATCH 24/24] remove stray line --- steamguard/src/steamapi.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index d44c69a..22cb78d 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -571,7 +571,6 @@ fn test_login_response_parse_missing_webcookie() { } #[derive(Debug, Clone, Deserialize)] -#[serde(transparent)] pub struct SteamApiResponse { pub response: T, }