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 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<String> = 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) => {

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.
///
/// `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<T>(input: T, chars: &str) -> anyhow::Result<char>
where
T: Into<String>,
{
fn prompt_char_impl(input: impl Into<String>, chars: &str) -> anyhow::Result<char> {
let uppers = chars.replace(char::is_lowercase, "");
if uppers.len() > 1 {
panic!("Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed.");

View file

@ -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<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::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<T: Transport>(
///
/// A convenience method for [`AccountLinker::remove_authenticator`].
pub fn remove_authenticator(
&self,
client: &TwoFactorClient<T>,
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<EResult> 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)
}
}