From 58897b6695ba714c4c0f0728440b61be7b55aa80 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 8 Aug 2021 18:32:50 -0400 Subject: [PATCH] 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 {}