From 166b7a908ce601e6ed04485f63d873e8f31a8068 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 3 Dec 2023 11:36:35 -0500 Subject: [PATCH] setup: add support for different link confirmation types, and verify that the authenticator was actually set up (#348) fixes #345 --- src/accountmanager.rs | 6 ++-- src/commands/remove.rs | 2 +- src/commands/setup.rs | 49 +++++++++++++++++++++++++++------ src/tui.rs | 10 +++++++ steamguard/src/accountlinker.rs | 45 ++++++++++++++++++++++++++++-- 5 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 106b713..bd95257 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -196,14 +196,14 @@ impl AccountManager { Ok(()) } - pub fn remove_account(&mut self, account_name: String) { + pub fn remove_account(&mut self, account_name: &String) { let index = self .manifest .entries .iter() - .position(|a| a.account_name == account_name) + .position(|a| &a.account_name == account_name) .unwrap(); - self.accounts.remove(&account_name); + self.accounts.remove(account_name); self.manifest.entries.remove(index); } diff --git a/src/commands/remove.rs b/src/commands/remove.rs index 35f7e15..d5f9eeb 100644 --- a/src/commands/remove.rs +++ b/src/commands/remove.rs @@ -94,7 +94,7 @@ where } for account_name in successful { - manager.remove_account(account_name); + manager.remove_account(&account_name); } manager.save()?; diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 283633f..73062d4 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -2,8 +2,11 @@ use log::*; use phonenumber::PhoneNumber; use secrecy::ExposeSecret; use steamguard::{ - accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient, - token::Tokens, AccountLinkError, AccountLinker, FinalizeLinkError, + accountlinker::{AccountLinkConfirmType, AccountLinkSuccess}, + phonelinker::PhoneLinker, + steamapi::PhoneClient, + token::Tokens, + AccountLinkError, AccountLinker, FinalizeLinkError, }; use crate::{tui, AccountManager}; @@ -48,6 +51,7 @@ where break; } Err(AccountLinkError::MustProvidePhoneNumber) => { + // As of Dec 12, 2023, Steam no longer appears to require a phone number to add an authenticator. Keeping this code here just in case. eprintln!("Looks like you don't have a phone number on this account."); do_add_phone_number(transport.clone(), linker.tokens())?; } @@ -66,6 +70,7 @@ where } let mut server_time = link.server_time(); let phone_number_hint = link.phone_number_hint().to_owned(); + let confirm_type = link.confirm_type(); manager.add_account(link.into_account()); match manager.save() { Ok(_) => {} @@ -88,15 +93,29 @@ where tui::pause(); debug!("attempting link finalization"); - println!( - "A code has been sent to your phone number ending in {}.", - phone_number_hint - ); - print!("Enter SMS code: "); - let sms_code = tui::prompt(); + let confirm_code = match confirm_type { + AccountLinkConfirmType::Email => { + eprintln!( + "A code has been sent to the email address associated with this account." + ); + tui::prompt_non_empty("Enter email code: ") + } + AccountLinkConfirmType::SMS => { + eprintln!( + "A code has been sent to your phone number ending in {}.", + phone_number_hint + ); + tui::prompt_non_empty("Enter SMS code: ") + } + AccountLinkConfirmType::Unknown(t) => { + error!("Unknown link confirm type: {}", t); + bail!("Unknown link confirm type: {}", t); + } + }; + let mut tries = 0; loop { - match linker.finalize(server_time, &mut account, sms_code.clone()) { + match linker.finalize(server_time, &mut account, confirm_code.clone()) { Ok(_) => break, Err(FinalizeLinkError::WantMore { server_time: s }) => { server_time = s; @@ -116,6 +135,18 @@ where let revocation_code = account.revocation_code.clone(); drop(account); // explicitly drop the lock so we don't hang on the mutex + info!("Verifying authenticator status..."); + let status = + linker.query_status(&manager.get_account(&account_name).unwrap().lock().unwrap())?; + if status.state() == 0 { + debug!( + "authenticator state: {} -- did not actually finalize", + status.state() + ); + manager.remove_account(&account_name); + bail!("Authenticator finalization was unsuccessful. You may have entered the wrong confirm code in the previous step. Try again."); + } + info!("Authenticator finalized."); match manager.save() { Ok(_) => {} diff --git a/src/tui.rs b/src/tui.rs index 99c3dd2..67c2db8 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -34,6 +34,16 @@ pub(crate) fn prompt() -> String { line } +pub(crate) fn prompt_non_empty(prompt_text: impl AsRef) -> String { + loop { + eprint!("{}", prompt_text.as_ref()); + let input = prompt(); + if !input.is_empty() { + return input; + } + } +} + /// Prompt the user for a single character response. Useful for asking yes or no questions. /// /// `chars` should be all lowercase characters, with at most 1 uppercase character. The uppercase character is the default answer if no answer is provided. diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 823666a..45b5e85 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,5 +1,6 @@ use crate::protobufs::service_twofactor::{ CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request, + CTwoFactor_Status_Request, CTwoFactor_Status_Response, }; use crate::steamapi::twofactor::TwoFactorClient; use crate::token::TwoFactorSecret; @@ -85,6 +86,7 @@ where account, server_time: resp.server_time(), phone_number_hint: resp.take_phone_number_hint(), + confirm_type: resp.confirm_type().into(), }; Ok(success) } @@ -94,7 +96,7 @@ where &mut self, time: u64, account: &mut SteamGuardAccount, - sms_code: String, + confirm_code: String, ) -> anyhow::Result<(), FinalizeLinkError> { let code = account.generate_code(time); @@ -105,7 +107,8 @@ where req.set_steamid(steam_id); req.set_authenticator_code(code); req.set_authenticator_time(time); - req.set_activation_code(sms_code); + req.set_activation_code(confirm_code); + req.set_validate_sms_code(true); let resp = self.client.finalize_authenticator(req, token)?; @@ -124,6 +127,21 @@ where self.finalized = true; Ok(()) } + + pub fn query_status( + &self, + account: &SteamGuardAccount, + ) -> anyhow::Result { + let mut req = CTwoFactor_Status_Request::new(); + req.set_steamid(account.steam_id); + + let resp = self + .client + .query_status(req, self.tokens.access_token()) + .unwrap(); + + Ok(resp.into_response_data()) + } } #[derive(Debug)] @@ -131,6 +149,7 @@ pub struct AccountLinkSuccess { account: SteamGuardAccount, server_time: u64, phone_number_hint: String, + confirm_type: AccountLinkConfirmType, } impl AccountLinkSuccess { @@ -149,6 +168,28 @@ impl AccountLinkSuccess { pub fn phone_number_hint(&self) -> &str { &self.phone_number_hint } + + pub fn confirm_type(&self) -> AccountLinkConfirmType { + self.confirm_type + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum AccountLinkConfirmType { + SMS = 1, + Email = 3, + Unknown(i32), +} + +impl From for AccountLinkConfirmType { + fn from(i: i32) -> Self { + match i { + 1 => AccountLinkConfirmType::SMS, + 3 => AccountLinkConfirmType::Email, + _ => AccountLinkConfirmType::Unknown(i), + } + } } fn generate_device_id() -> String {