From db2ec59c0713246b507cba9431d3e3741aa02493 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 9 Aug 2021 00:09:34 -0400 Subject: [PATCH] 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, +}