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:
parent
4ea44151b9
commit
225b53d719
3 changed files with 232 additions and 16 deletions
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue