diff --git a/src/commands/remove.rs b/src/commands/remove.rs index d5f9eeb..f5802b2 100644 --- a/src/commands/remove.rs +++ b/src/commands/remove.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, Mutex}; use log::*; -use steamguard::{steamapi::TwoFactorClient, transport::TransportError, RemoveAuthenticatorError}; +use steamguard::{accountlinker::RemoveAuthenticatorError, transport::TransportError}; use crate::{errors::UserError, tui, AccountManager}; @@ -43,11 +43,10 @@ where let mut successful = vec![]; for a in accounts { let mut account = a.lock().unwrap(); - let client = TwoFactorClient::new(transport.clone()); let mut revocation: Option = None; loop { - match account.remove_authenticator(&client, revocation.as_ref()) { + match account.remove_authenticator(transport.clone(), revocation.as_ref()) { Ok(_) => { info!("Removed authenticator from {}", account.account_name); successful.push(account.account_name.clone()); @@ -69,17 +68,17 @@ where error!("No attempts remaining, aborting!"); break; } - eprint!("Enter the revocation code for {}: ", account.account_name); - let code = tui::prompt(); + let code = tui::prompt_non_empty(format!( + "Enter the revocation code for {}: ", + account.account_name + )); revocation = Some(code); } Err(RemoveAuthenticatorError::MissingRevocationCode) => { - error!( - "Account {} does not have a revocation code", + let code = tui::prompt_non_empty(format!( + "Enter the revocation code for {}: ", account.account_name - ); - eprint!("Enter the revocation code for {}: ", account.account_name); - let code = tui::prompt(); + )); revocation = Some(code); } Err(err) => { diff --git a/src/tui.rs b/src/tui.rs index 67c2db8..8c69aa4 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -47,6 +47,7 @@ pub(crate) fn prompt_non_empty(prompt_text: impl AsRef) -> String { /// 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. +/// The selected character returned will always be lowercase. pub(crate) fn prompt_char(text: &str, chars: &str) -> char { loop { let _ = stderr().queue(Print(format!("{} [{}] ", text, chars))); @@ -58,10 +59,7 @@ pub(crate) fn prompt_char(text: &str, chars: &str) -> char { } } -fn prompt_char_impl(input: T, chars: &str) -> anyhow::Result -where - T: Into, -{ +fn prompt_char_impl(input: impl Into, chars: &str) -> anyhow::Result { let uppers = chars.replace(char::is_lowercase, ""); if uppers.len() > 1 { panic!("Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed."); diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 45b5e85..a7b3975 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -1,10 +1,10 @@ use crate::protobufs::service_twofactor::{ CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request, - CTwoFactor_Status_Request, CTwoFactor_Status_Response, + CTwoFactor_RemoveAuthenticator_Request, CTwoFactor_Status_Request, CTwoFactor_Status_Response, }; use crate::steamapi::twofactor::TwoFactorClient; use crate::token::TwoFactorSecret; -use crate::transport::Transport; +use crate::transport::{Transport, TransportError}; use crate::{steamapi::EResult, token::Tokens, SteamGuardAccount}; use anyhow::Context; use base64::Engine; @@ -142,6 +142,36 @@ where Ok(resp.into_response_data()) } + + pub fn remove_authenticator( + &self, + revocation_code: Option<&String>, + ) -> Result<(), RemoveAuthenticatorError> { + let Some(revocation_code) = revocation_code else { + return Err(RemoveAuthenticatorError::MissingRevocationCode); + }; + if revocation_code.is_empty() { + return Err(RemoveAuthenticatorError::MissingRevocationCode); + } + let mut req = CTwoFactor_RemoveAuthenticator_Request::new(); + req.set_revocation_code(revocation_code.clone()); + let resp = self + .client + .remove_authenticator(req, self.tokens.access_token())?; + + // returns EResult::TwoFactorCodeMismatch if the revocation code is incorrect + if resp.result != EResult::OK && resp.result != EResult::TwoFactorCodeMismatch { + return Err(resp.result.into()); + } + let resp = resp.into_response_data(); + if !resp.success() { + return Err(RemoveAuthenticatorError::IncorrectRevocationCode { + attempts_remaining: resp.revocation_attempts_remaining(), + }); + } + + Ok(()) + } } #[derive(Debug)] @@ -253,3 +283,23 @@ impl From for FinalizeLinkError { } } } + +#[derive(Debug, thiserror::Error)] +pub enum RemoveAuthenticatorError { + #[error("Missing revocation code")] + MissingRevocationCode, + #[error("Incorrect revocation code, {attempts_remaining} attempts remaining")] + IncorrectRevocationCode { attempts_remaining: u32 }, + #[error("Transport error: {0}")] + TransportError(#[from] TransportError), + #[error("Steam returned an enexpected result: {0:?}")] + UnknownEResult(EResult), + #[error("Unexpected error: {0}")] + Unknown(#[from] anyhow::Error), +} + +impl From for RemoveAuthenticatorError { + fn from(e: EResult) -> Self { + Self::UnknownEResult(e) + } +} diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index d26138e..8c72ff3 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -1,6 +1,5 @@ -use crate::protobufs::service_twofactor::CTwoFactor_RemoveAuthenticator_Request; -use crate::steamapi::EResult; -use crate::{steamapi::twofactor::TwoFactorClient, token::TwoFactorSecret}; +use crate::token::TwoFactorSecret; +use accountlinker::RemoveAuthenticatorError; pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError}; pub use confirmation::*; pub use qrapprover::{QrApprover, QrApproverError}; @@ -96,58 +95,21 @@ impl SteamGuardAccount { /// Removes the mobile authenticator from the steam account. If this operation succeeds, this object can no longer be considered valid. /// Returns whether or not the operation was successful. - pub fn remove_authenticator( + /// + /// A convenience method for [`AccountLinker::remove_authenticator`]. + pub fn remove_authenticator( &self, - client: &TwoFactorClient, + transport: impl Transport, revocation_code: Option<&String>, ) -> Result<(), RemoveAuthenticatorError> { - if revocation_code.is_none() && self.revocation_code.expose_secret().is_empty() { - return Err(RemoveAuthenticatorError::MissingRevocationCode); - } let Some(tokens) = &self.tokens else { return Err(RemoveAuthenticatorError::TransportError( TransportError::Unauthorized, )); }; - let mut req = CTwoFactor_RemoveAuthenticator_Request::new(); - req.set_revocation_code( - revocation_code - .unwrap_or(self.revocation_code.expose_secret()) - .to_owned(), - ); - let resp = client.remove_authenticator(req, tokens.access_token())?; - - // returns EResult::TwoFactorCodeMismatch if the revocation code is incorrect - if resp.result != EResult::OK && resp.result != EResult::TwoFactorCodeMismatch { - return Err(resp.result.into()); - } - let resp = resp.into_response_data(); - if !resp.success() { - return Err(RemoveAuthenticatorError::IncorrectRevocationCode { - attempts_remaining: resp.revocation_attempts_remaining(), - }); - } - - Ok(()) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum RemoveAuthenticatorError { - #[error("Missing revocation code")] - MissingRevocationCode, - #[error("Incorrect revocation code, {attempts_remaining} attempts remaining")] - IncorrectRevocationCode { attempts_remaining: u32 }, - #[error("Transport error: {0}")] - TransportError(#[from] TransportError), - #[error("Steam returned an enexpected result: {0:?}")] - UnknownEResult(EResult), - #[error("Unexpected error: {0}")] - Unknown(#[from] anyhow::Error), -} - -impl From for RemoveAuthenticatorError { - fn from(e: EResult) -> Self { - Self::UnknownEResult(e) + let revocation_code = + Some(revocation_code.unwrap_or_else(|| self.revocation_code.expose_secret())); + let linker = AccountLinker::new(transport, tokens.clone()); + linker.remove_authenticator(revocation_code) } }