setup: add support for transfering or removing authenticators (#354)

- add methods for remove auth via challenge
- add `transfer_start` and `transfer_finish` to account linker
This commit is contained in:
Carson McManus 2023-12-08 12:38:36 -05:00 committed by GitHub
parent 4ea44151b9
commit 225b53d719
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 232 additions and 16 deletions

View file

@ -2,7 +2,7 @@ use log::*;
use phonenumber::PhoneNumber;
use secrecy::ExposeSecret;
use steamguard::{
accountlinker::{AccountLinkConfirmType, AccountLinkSuccess},
accountlinker::{AccountLinkConfirmType, AccountLinkSuccess, RemoveAuthenticatorError},
phonelinker::PhoneLinker,
steamapi::PhoneClient,
token::Tokens,
@ -43,12 +43,10 @@ where
info!("Adding authenticator...");
let mut linker = AccountLinker::new(transport.clone(), tokens);
let link: AccountLinkSuccess;
loop {
match linker.link() {
Ok(a) => {
link = a;
break;
Ok(link) => {
return Self::add_new_account(link, manager, account_name, linker);
}
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.
@ -59,6 +57,49 @@ where
println!("Check your email and click the link.");
tui::pause();
}
Err(AccountLinkError::AuthenticatorPresent) => {
eprintln!("It looks like there's already an authenticator on this account. If you want to link it to steamguard-cli, you'll need to remove it first. If you remove it using your revocation code (R#####), you'll get a 15 day trade ban.");
eprintln!("However, you can \"transfer\" the authenticator to steamguard-cli if you have access to the phone number associated with your account. This will cause you to get only a 2 day trade ban.");
eprintln!("If you were using SDA or WinAuth, you can import it into steamguard-cli with the `import` command, and have no trade ban.");
eprintln!("You can't have the same authenticator on steamguard-cli and the steam mobile app at the same time.");
eprintln!("\nHere are your options:");
eprintln!("[T] Transfer authenticator to steamguard-cli (2 day trade ban)");
eprintln!("[R] Revoke authenticator with revocation code (15 day trade ban)");
eprintln!("[A] Abort setup");
let answer = tui::prompt_char("What would you like to do?", "Tra");
match answer {
't' => return Self::transfer_new_account(linker, manager),
'r' => {
loop {
let revocation_code =
tui::prompt_non_empty("Enter your revocation code (R#####): ");
match linker.remove_authenticator(Some(&revocation_code)) {
Ok(_) => break,
Err(RemoveAuthenticatorError::IncorrectRevocationCode {
attempts_remaining,
}) => {
error!(
"Revocation code was incorrect ({} attempts remaining)",
attempts_remaining
);
if attempts_remaining == 0 {
error!("No attempts remaining, aborting!");
bail!("Failed to remove authenticator: no attempts remaining")
}
}
Err(err) => {
error!("Failed to remove authenticator: {}", err);
}
}
}
}
_ => {
info!("Aborting account linking.");
return Err(AccountLinkError::AuthenticatorPresent.into());
}
}
}
Err(err) => {
error!(
"Failed to link authenticator. Account has not been linked. {}",
@ -68,6 +109,20 @@ where
}
}
}
}
}
impl SetupCommand {
/// Add a new account to the manifest after linking has started.
fn add_new_account<T>(
link: AccountLinkSuccess,
manager: &mut AccountManager,
account_name: String,
mut linker: AccountLinker<T>,
) -> Result<(), anyhow::Error>
where
T: Transport + Clone,
{
let mut server_time = link.server_time();
let phone_number_hint = link.phone_number_hint().to_owned();
let confirm_type = link.confirm_type();
@ -77,21 +132,18 @@ where
Err(err) => {
error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err);
eprintln!(
"Just in case, here is the account info. Save it somewhere just in case!\n{:#?}",
manager.get_account(&account_name).unwrap().lock().unwrap()
);
"Just in case, here is the account info. Save it somewhere just in case!\n{:#?}",
manager.get_account(&account_name).unwrap().lock().unwrap()
);
return Err(err);
}
}
let account_arc = manager
.get_account(&account_name)
.expect("account was not present in manifest");
let mut account = account_arc.lock().unwrap();
eprintln!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code.expose_secret());
tui::pause();
debug!("attempting link finalization");
let confirm_code = match confirm_type {
AccountLinkConfirmType::Email => {
@ -112,7 +164,6 @@ where
bail!("Unknown link confirm type: {}", t);
}
};
let mut tries = 0;
loop {
match linker.finalize(server_time, &mut account, confirm_code.clone()) {
@ -133,8 +184,7 @@ where
}
}
let revocation_code = account.revocation_code.clone();
drop(account); // explicitly drop the lock so we don't hang on the mutex
drop(account);
info!("Verifying authenticator status...");
let status =
linker.query_status(&manager.get_account(&account_name).unwrap().lock().unwrap())?;
@ -147,7 +197,6 @@ where
manager.save()?;
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(_) => {}
@ -159,12 +208,52 @@ where
return Err(err);
}
}
eprintln!(
"Authenticator has been finalized. Please actually write down your revocation code: {}",
revocation_code.expose_secret()
);
Ok(())
}
/// Transfer an existing authenticator to steamguard-cli.
fn transfer_new_account<T>(
mut linker: AccountLinker<T>,
manager: &mut AccountManager,
) -> anyhow::Result<()>
where
T: Transport + Clone,
{
info!("Transferring authenticator to steamguard-cli");
linker.transfer_start()?;
let account: SteamGuardAccount;
loop {
let sms_code = tui::prompt_non_empty("Enter SMS code: ");
match linker.transfer_finish(sms_code) {
Ok(acc) => {
account = acc;
break;
}
Err(err) => {
error!("Failed to transfer authenticator: {}", err);
}
}
}
info!("Transfer successful, adding account to manifest");
let revocation_code = account.revocation_code.clone();
eprintln!(
"Take a moment to write down your revocation code: {}",
revocation_code.expose_secret()
);
manager.add_account(account);
manager.save()?;
eprintln!(
"Make sure you have your revocation code written down: {}",
revocation_code.expose_secret()
);
Ok(())
}
}

View file

@ -1,5 +1,7 @@
use crate::protobufs::service_twofactor::{
CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request,
CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Request,
CTwoFactor_RemoveAuthenticatorViaChallengeStart_Request,
CTwoFactor_RemoveAuthenticator_Request, CTwoFactor_Status_Request, CTwoFactor_Status_Response,
};
use crate::steamapi::twofactor::TwoFactorClient;
@ -172,6 +174,63 @@ where
Ok(())
}
/// Begin the process of "transfering" a mobile authenticator from a different device to this device.
///
/// "Transfering" does not actually literally transfer the secrets from one device to another. Instead, it generates a new set of secrets on this device, and invalidates the old secrets on the other device. Call [`Self::transfer_finish`] to complete the process.
pub fn transfer_start(&mut self) -> Result<(), TransferError> {
let req = CTwoFactor_RemoveAuthenticatorViaChallengeStart_Request::new();
let resp = self
.client
.remove_authenticator_via_challenge_start(req, self.tokens().access_token())?;
if resp.result != EResult::OK {
return Err(resp.result.into());
}
// the success field in the response is always None, so we can't check that
// it appears to not be used at all
Ok(())
}
/// Completes the process of "transfering" a mobile authenticator from a different device to this device.
pub fn transfer_finish(
&mut self,
sms_code: impl AsRef<str>,
) -> Result<SteamGuardAccount, TransferError> {
let access_token = self.tokens.access_token();
let steam_id = access_token
.decode()
.context("decoding access token")?
.steam_id();
let mut req = CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Request::new();
req.set_sms_code(sms_code.as_ref().to_owned());
req.set_generate_new_token(true);
let resp = self
.client
.remove_authenticator_via_challenge_continue(req, access_token)?;
if resp.result != EResult::OK {
return Err(resp.result.into());
}
let resp = resp.into_response_data();
let mut resp = resp.replacement_token.clone().unwrap();
let account = SteamGuardAccount {
account_name: resp.take_account_name(),
steam_id,
serial_number: resp.serial_number().to_string(),
revocation_code: resp.take_revocation_code().into(),
uri: resp.take_uri().into(),
shared_secret: TwoFactorSecret::from_bytes(resp.take_shared_secret()),
token_gid: resp.take_token_gid(),
identity_secret: base64::engine::general_purpose::STANDARD
.encode(resp.take_identity_secret())
.into(),
device_id: self.device_id.clone(),
secret_1: base64::engine::general_purpose::STANDARD
.encode(resp.take_secret_1())
.into(),
tokens: Some(self.tokens.clone()),
};
Ok(account)
}
}
#[derive(Debug)]
@ -303,3 +362,24 @@ impl From<EResult> for RemoveAuthenticatorError {
Self::UnknownEResult(e)
}
}
#[derive(Error, Debug)]
pub enum TransferError {
#[error("Provided SMS code was incorrect.")]
BadSmsCode,
#[error("Failed to send request to Steam: {0:?}")]
Transport(#[from] crate::transport::TransportError),
#[error("Steam returned an unexpected error code: {0:?}")]
UnknownEResult(EResult),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}
impl From<EResult> for TransferError {
fn from(result: EResult) -> Self {
match result {
EResult::SMSCodeFailed => TransferError::BadSmsCode,
r => TransferError::UnknownEResult(r),
}
}
}

View file

@ -69,6 +69,45 @@ where
Ok(resp)
}
pub fn remove_authenticator_via_challenge_start(
&self,
req: CTwoFactor_RemoveAuthenticatorViaChallengeStart_Request,
access_token: &Jwt,
) -> Result<ApiResponse<CTwoFactor_RemoveAuthenticatorViaChallengeStart_Response>, TransportError>
{
let req = ApiRequest::new(SERVICE_NAME, "RemoveAuthenticatorViaChallengeStart", 1, req)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CTwoFactor_RemoveAuthenticatorViaChallengeStart_Request, CTwoFactor_RemoveAuthenticatorViaChallengeStart_Response>(
req,
)?;
Ok(resp)
}
pub fn remove_authenticator_via_challenge_continue(
&self,
req: CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Request,
access_token: &Jwt,
) -> Result<
ApiResponse<CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Response>,
TransportError,
> {
let req = ApiRequest::new(
SERVICE_NAME,
"RemoveAuthenticatorViaChallengeContinue",
1,
req,
)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Request, CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Response>(
req,
)?;
Ok(resp)
}
pub fn query_status(
&self,
req: CTwoFactor_Status_Request,
@ -108,5 +147,13 @@ macro_rules! impl_buildable_req {
impl_buildable_req!(CTwoFactor_AddAuthenticator_Request, true);
impl_buildable_req!(CTwoFactor_FinalizeAddAuthenticator_Request, true);
impl_buildable_req!(CTwoFactor_RemoveAuthenticator_Request, true);
impl_buildable_req!(
CTwoFactor_RemoveAuthenticatorViaChallengeStart_Request,
true
);
impl_buildable_req!(
CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Request,
true
);
impl_buildable_req!(CTwoFactor_Status_Request, true);
impl_buildable_req!(CTwoFactor_Time_Request, false);