remove: move main remove_authenticator request logic into AccountLinker (#353)

This commit is contained in:
Carson McManus 2023-12-08 11:39:41 -05:00 committed by GitHub
parent 9358fb60c9
commit 68dcedaeb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 73 additions and 64 deletions

View file

@ -1,7 +1,7 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use log::*; use log::*;
use steamguard::{steamapi::TwoFactorClient, transport::TransportError, RemoveAuthenticatorError}; use steamguard::{accountlinker::RemoveAuthenticatorError, transport::TransportError};
use crate::{errors::UserError, tui, AccountManager}; use crate::{errors::UserError, tui, AccountManager};
@ -43,11 +43,10 @@ where
let mut successful = vec![]; let mut successful = vec![];
for a in accounts { for a in accounts {
let mut account = a.lock().unwrap(); let mut account = a.lock().unwrap();
let client = TwoFactorClient::new(transport.clone());
let mut revocation: Option<String> = None; let mut revocation: Option<String> = None;
loop { loop {
match account.remove_authenticator(&client, revocation.as_ref()) { match account.remove_authenticator(transport.clone(), revocation.as_ref()) {
Ok(_) => { Ok(_) => {
info!("Removed authenticator from {}", account.account_name); info!("Removed authenticator from {}", account.account_name);
successful.push(account.account_name.clone()); successful.push(account.account_name.clone());
@ -69,17 +68,17 @@ where
error!("No attempts remaining, aborting!"); error!("No attempts remaining, aborting!");
break; break;
} }
eprint!("Enter the revocation code for {}: ", account.account_name); let code = tui::prompt_non_empty(format!(
let code = tui::prompt(); "Enter the revocation code for {}: ",
account.account_name
));
revocation = Some(code); revocation = Some(code);
} }
Err(RemoveAuthenticatorError::MissingRevocationCode) => { Err(RemoveAuthenticatorError::MissingRevocationCode) => {
error!( let code = tui::prompt_non_empty(format!(
"Account {} does not have a revocation code", "Enter the revocation code for {}: ",
account.account_name account.account_name
); ));
eprint!("Enter the revocation code for {}: ", account.account_name);
let code = tui::prompt();
revocation = Some(code); revocation = Some(code);
} }
Err(err) => { Err(err) => {

View file

@ -47,6 +47,7 @@ pub(crate) fn prompt_non_empty(prompt_text: impl AsRef<str>) -> String {
/// Prompt the user for a single character response. Useful for asking yes or no questions. /// 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. /// `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 { pub(crate) fn prompt_char(text: &str, chars: &str) -> char {
loop { loop {
let _ = stderr().queue(Print(format!("{} [{}] ", text, chars))); 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<T>(input: T, chars: &str) -> anyhow::Result<char> fn prompt_char_impl(input: impl Into<String>, chars: &str) -> anyhow::Result<char> {
where
T: Into<String>,
{
let uppers = chars.replace(char::is_lowercase, ""); let uppers = chars.replace(char::is_lowercase, "");
if uppers.len() > 1 { if uppers.len() > 1 {
panic!("Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed."); panic!("Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed.");

View file

@ -1,10 +1,10 @@
use crate::protobufs::service_twofactor::{ use crate::protobufs::service_twofactor::{
CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request, 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::steamapi::twofactor::TwoFactorClient;
use crate::token::TwoFactorSecret; use crate::token::TwoFactorSecret;
use crate::transport::Transport; use crate::transport::{Transport, TransportError};
use crate::{steamapi::EResult, token::Tokens, SteamGuardAccount}; use crate::{steamapi::EResult, token::Tokens, SteamGuardAccount};
use anyhow::Context; use anyhow::Context;
use base64::Engine; use base64::Engine;
@ -142,6 +142,36 @@ where
Ok(resp.into_response_data()) 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)] #[derive(Debug)]
@ -253,3 +283,23 @@ impl From<EResult> 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<EResult> for RemoveAuthenticatorError {
fn from(e: EResult) -> Self {
Self::UnknownEResult(e)
}
}

View file

@ -1,6 +1,5 @@
use crate::protobufs::service_twofactor::CTwoFactor_RemoveAuthenticator_Request; use crate::token::TwoFactorSecret;
use crate::steamapi::EResult; use accountlinker::RemoveAuthenticatorError;
use crate::{steamapi::twofactor::TwoFactorClient, token::TwoFactorSecret};
pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError}; pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError};
pub use confirmation::*; pub use confirmation::*;
pub use qrapprover::{QrApprover, QrApproverError}; 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. /// 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. /// Returns whether or not the operation was successful.
pub fn remove_authenticator<T: Transport>( ///
/// A convenience method for [`AccountLinker::remove_authenticator`].
pub fn remove_authenticator(
&self, &self,
client: &TwoFactorClient<T>, transport: impl Transport,
revocation_code: Option<&String>, revocation_code: Option<&String>,
) -> Result<(), RemoveAuthenticatorError> { ) -> Result<(), RemoveAuthenticatorError> {
if revocation_code.is_none() && self.revocation_code.expose_secret().is_empty() {
return Err(RemoveAuthenticatorError::MissingRevocationCode);
}
let Some(tokens) = &self.tokens else { let Some(tokens) = &self.tokens else {
return Err(RemoveAuthenticatorError::TransportError( return Err(RemoveAuthenticatorError::TransportError(
TransportError::Unauthorized, TransportError::Unauthorized,
)); ));
}; };
let mut req = CTwoFactor_RemoveAuthenticator_Request::new(); let revocation_code =
req.set_revocation_code( Some(revocation_code.unwrap_or_else(|| self.revocation_code.expose_secret()));
revocation_code let linker = AccountLinker::new(transport, tokens.clone());
.unwrap_or(self.revocation_code.expose_secret()) linker.remove_authenticator(revocation_code)
.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<EResult> for RemoveAuthenticatorError {
fn from(e: EResult) -> Self {
Self::UnknownEResult(e)
} }
} }