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 phonenumber::PhoneNumber;
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
use steamguard::{
|
use steamguard::{
|
||||||
accountlinker::{AccountLinkConfirmType, AccountLinkSuccess},
|
accountlinker::{AccountLinkConfirmType, AccountLinkSuccess, RemoveAuthenticatorError},
|
||||||
phonelinker::PhoneLinker,
|
phonelinker::PhoneLinker,
|
||||||
steamapi::PhoneClient,
|
steamapi::PhoneClient,
|
||||||
token::Tokens,
|
token::Tokens,
|
||||||
|
@ -43,12 +43,10 @@ where
|
||||||
|
|
||||||
info!("Adding authenticator...");
|
info!("Adding authenticator...");
|
||||||
let mut linker = AccountLinker::new(transport.clone(), tokens);
|
let mut linker = AccountLinker::new(transport.clone(), tokens);
|
||||||
let link: AccountLinkSuccess;
|
|
||||||
loop {
|
loop {
|
||||||
match linker.link() {
|
match linker.link() {
|
||||||
Ok(a) => {
|
Ok(link) => {
|
||||||
link = a;
|
return Self::add_new_account(link, manager, account_name, linker);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
Err(AccountLinkError::MustProvidePhoneNumber) => {
|
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.
|
// 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.");
|
println!("Check your email and click the link.");
|
||||||
tui::pause();
|
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) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
"Failed to link authenticator. Account has not been linked. {}",
|
"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 mut server_time = link.server_time();
|
||||||
let phone_number_hint = link.phone_number_hint().to_owned();
|
let phone_number_hint = link.phone_number_hint().to_owned();
|
||||||
let confirm_type = link.confirm_type();
|
let confirm_type = link.confirm_type();
|
||||||
|
@ -77,21 +132,18 @@ where
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err);
|
error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Just in case, here is the account info. Save it somewhere just in case!\n{:#?}",
|
"Just in case, here is the account info. Save it somewhere just in case!\n{:#?}",
|
||||||
manager.get_account(&account_name).unwrap().lock().unwrap()
|
manager.get_account(&account_name).unwrap().lock().unwrap()
|
||||||
);
|
);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let account_arc = manager
|
let account_arc = manager
|
||||||
.get_account(&account_name)
|
.get_account(&account_name)
|
||||||
.expect("account was not present in manifest");
|
.expect("account was not present in manifest");
|
||||||
let mut account = account_arc.lock().unwrap();
|
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());
|
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();
|
tui::pause();
|
||||||
|
|
||||||
debug!("attempting link finalization");
|
debug!("attempting link finalization");
|
||||||
let confirm_code = match confirm_type {
|
let confirm_code = match confirm_type {
|
||||||
AccountLinkConfirmType::Email => {
|
AccountLinkConfirmType::Email => {
|
||||||
|
@ -112,7 +164,6 @@ where
|
||||||
bail!("Unknown link confirm type: {}", t);
|
bail!("Unknown link confirm type: {}", t);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut tries = 0;
|
let mut tries = 0;
|
||||||
loop {
|
loop {
|
||||||
match linker.finalize(server_time, &mut account, confirm_code.clone()) {
|
match linker.finalize(server_time, &mut account, confirm_code.clone()) {
|
||||||
|
@ -133,8 +184,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let revocation_code = account.revocation_code.clone();
|
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...");
|
info!("Verifying authenticator status...");
|
||||||
let status =
|
let status =
|
||||||
linker.query_status(&manager.get_account(&account_name).unwrap().lock().unwrap())?;
|
linker.query_status(&manager.get_account(&account_name).unwrap().lock().unwrap())?;
|
||||||
|
@ -147,7 +197,6 @@ where
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
bail!("Authenticator finalization was unsuccessful. You may have entered the wrong confirm code in the previous step. Try again.");
|
bail!("Authenticator finalization was unsuccessful. You may have entered the wrong confirm code in the previous step. Try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Authenticator finalized.");
|
info!("Authenticator finalized.");
|
||||||
match manager.save() {
|
match manager.save() {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
|
@ -159,12 +208,52 @@ where
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Authenticator has been finalized. Please actually write down your revocation code: {}",
|
"Authenticator has been finalized. Please actually write down your revocation code: {}",
|
||||||
revocation_code.expose_secret()
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::protobufs::service_twofactor::{
|
use crate::protobufs::service_twofactor::{
|
||||||
CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request,
|
CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request,
|
||||||
|
CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Request,
|
||||||
|
CTwoFactor_RemoveAuthenticatorViaChallengeStart_Request,
|
||||||
CTwoFactor_RemoveAuthenticator_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;
|
||||||
|
@ -172,6 +174,63 @@ where
|
||||||
|
|
||||||
Ok(())
|
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)]
|
#[derive(Debug)]
|
||||||
|
@ -303,3 +362,24 @@ impl From<EResult> for RemoveAuthenticatorError {
|
||||||
Self::UnknownEResult(e)
|
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)
|
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(
|
pub fn query_status(
|
||||||
&self,
|
&self,
|
||||||
req: CTwoFactor_Status_Request,
|
req: CTwoFactor_Status_Request,
|
||||||
|
@ -108,5 +147,13 @@ macro_rules! impl_buildable_req {
|
||||||
impl_buildable_req!(CTwoFactor_AddAuthenticator_Request, true);
|
impl_buildable_req!(CTwoFactor_AddAuthenticator_Request, true);
|
||||||
impl_buildable_req!(CTwoFactor_FinalizeAddAuthenticator_Request, true);
|
impl_buildable_req!(CTwoFactor_FinalizeAddAuthenticator_Request, true);
|
||||||
impl_buildable_req!(CTwoFactor_RemoveAuthenticator_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_Status_Request, true);
|
||||||
impl_buildable_req!(CTwoFactor_Time_Request, false);
|
impl_buildable_req!(CTwoFactor_Time_Request, false);
|
||||||
|
|
Loading…
Reference in a new issue