setup: add support for different link confirmation types, and verify that the authenticator was actually set up (#348)
fixes #345
This commit is contained in:
parent
63805293ff
commit
166b7a908c
5 changed files with 97 additions and 15 deletions
|
@ -196,14 +196,14 @@ impl AccountManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_account(&mut self, account_name: String) {
|
pub fn remove_account(&mut self, account_name: &String) {
|
||||||
let index = self
|
let index = self
|
||||||
.manifest
|
.manifest
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|a| a.account_name == account_name)
|
.position(|a| &a.account_name == account_name)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.accounts.remove(&account_name);
|
self.accounts.remove(account_name);
|
||||||
self.manifest.entries.remove(index);
|
self.manifest.entries.remove(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
for account_name in successful {
|
for account_name in successful {
|
||||||
manager.remove_account(account_name);
|
manager.remove_account(&account_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
|
|
|
@ -2,8 +2,11 @@ use log::*;
|
||||||
use phonenumber::PhoneNumber;
|
use phonenumber::PhoneNumber;
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
use steamguard::{
|
use steamguard::{
|
||||||
accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient,
|
accountlinker::{AccountLinkConfirmType, AccountLinkSuccess},
|
||||||
token::Tokens, AccountLinkError, AccountLinker, FinalizeLinkError,
|
phonelinker::PhoneLinker,
|
||||||
|
steamapi::PhoneClient,
|
||||||
|
token::Tokens,
|
||||||
|
AccountLinkError, AccountLinker, FinalizeLinkError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{tui, AccountManager};
|
use crate::{tui, AccountManager};
|
||||||
|
@ -48,6 +51,7 @@ where
|
||||||
break;
|
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.
|
||||||
eprintln!("Looks like you don't have a phone number on this account.");
|
eprintln!("Looks like you don't have a phone number on this account.");
|
||||||
do_add_phone_number(transport.clone(), linker.tokens())?;
|
do_add_phone_number(transport.clone(), linker.tokens())?;
|
||||||
}
|
}
|
||||||
|
@ -66,6 +70,7 @@ where
|
||||||
}
|
}
|
||||||
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();
|
||||||
manager.add_account(link.into_account());
|
manager.add_account(link.into_account());
|
||||||
match manager.save() {
|
match manager.save() {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
|
@ -88,15 +93,29 @@ where
|
||||||
tui::pause();
|
tui::pause();
|
||||||
|
|
||||||
debug!("attempting link finalization");
|
debug!("attempting link finalization");
|
||||||
println!(
|
let confirm_code = match confirm_type {
|
||||||
"A code has been sent to your phone number ending in {}.",
|
AccountLinkConfirmType::Email => {
|
||||||
phone_number_hint
|
eprintln!(
|
||||||
);
|
"A code has been sent to the email address associated with this account."
|
||||||
print!("Enter SMS code: ");
|
);
|
||||||
let sms_code = tui::prompt();
|
tui::prompt_non_empty("Enter email code: ")
|
||||||
|
}
|
||||||
|
AccountLinkConfirmType::SMS => {
|
||||||
|
eprintln!(
|
||||||
|
"A code has been sent to your phone number ending in {}.",
|
||||||
|
phone_number_hint
|
||||||
|
);
|
||||||
|
tui::prompt_non_empty("Enter SMS code: ")
|
||||||
|
}
|
||||||
|
AccountLinkConfirmType::Unknown(t) => {
|
||||||
|
error!("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, sms_code.clone()) {
|
match linker.finalize(server_time, &mut account, confirm_code.clone()) {
|
||||||
Ok(_) => break,
|
Ok(_) => break,
|
||||||
Err(FinalizeLinkError::WantMore { server_time: s }) => {
|
Err(FinalizeLinkError::WantMore { server_time: s }) => {
|
||||||
server_time = s;
|
server_time = s;
|
||||||
|
@ -116,6 +135,18 @@ 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); // explicitly drop the lock so we don't hang on the mutex
|
||||||
|
|
||||||
|
info!("Verifying authenticator status...");
|
||||||
|
let status =
|
||||||
|
linker.query_status(&manager.get_account(&account_name).unwrap().lock().unwrap())?;
|
||||||
|
if status.state() == 0 {
|
||||||
|
debug!(
|
||||||
|
"authenticator state: {} -- did not actually finalize",
|
||||||
|
status.state()
|
||||||
|
);
|
||||||
|
manager.remove_account(&account_name);
|
||||||
|
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(_) => {}
|
||||||
|
|
10
src/tui.rs
10
src/tui.rs
|
@ -34,6 +34,16 @@ pub(crate) fn prompt() -> String {
|
||||||
line
|
line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prompt_non_empty(prompt_text: impl AsRef<str>) -> String {
|
||||||
|
loop {
|
||||||
|
eprint!("{}", prompt_text.as_ref());
|
||||||
|
let input = prompt();
|
||||||
|
if !input.is_empty() {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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,
|
||||||
};
|
};
|
||||||
use crate::steamapi::twofactor::TwoFactorClient;
|
use crate::steamapi::twofactor::TwoFactorClient;
|
||||||
use crate::token::TwoFactorSecret;
|
use crate::token::TwoFactorSecret;
|
||||||
|
@ -85,6 +86,7 @@ where
|
||||||
account,
|
account,
|
||||||
server_time: resp.server_time(),
|
server_time: resp.server_time(),
|
||||||
phone_number_hint: resp.take_phone_number_hint(),
|
phone_number_hint: resp.take_phone_number_hint(),
|
||||||
|
confirm_type: resp.confirm_type().into(),
|
||||||
};
|
};
|
||||||
Ok(success)
|
Ok(success)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +96,7 @@ where
|
||||||
&mut self,
|
&mut self,
|
||||||
time: u64,
|
time: u64,
|
||||||
account: &mut SteamGuardAccount,
|
account: &mut SteamGuardAccount,
|
||||||
sms_code: String,
|
confirm_code: String,
|
||||||
) -> anyhow::Result<(), FinalizeLinkError> {
|
) -> anyhow::Result<(), FinalizeLinkError> {
|
||||||
let code = account.generate_code(time);
|
let code = account.generate_code(time);
|
||||||
|
|
||||||
|
@ -105,7 +107,8 @@ where
|
||||||
req.set_steamid(steam_id);
|
req.set_steamid(steam_id);
|
||||||
req.set_authenticator_code(code);
|
req.set_authenticator_code(code);
|
||||||
req.set_authenticator_time(time);
|
req.set_authenticator_time(time);
|
||||||
req.set_activation_code(sms_code);
|
req.set_activation_code(confirm_code);
|
||||||
|
req.set_validate_sms_code(true);
|
||||||
|
|
||||||
let resp = self.client.finalize_authenticator(req, token)?;
|
let resp = self.client.finalize_authenticator(req, token)?;
|
||||||
|
|
||||||
|
@ -124,6 +127,21 @@ where
|
||||||
self.finalized = true;
|
self.finalized = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn query_status(
|
||||||
|
&self,
|
||||||
|
account: &SteamGuardAccount,
|
||||||
|
) -> anyhow::Result<CTwoFactor_Status_Response> {
|
||||||
|
let mut req = CTwoFactor_Status_Request::new();
|
||||||
|
req.set_steamid(account.steam_id);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.query_status(req, self.tokens.access_token())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(resp.into_response_data())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -131,6 +149,7 @@ pub struct AccountLinkSuccess {
|
||||||
account: SteamGuardAccount,
|
account: SteamGuardAccount,
|
||||||
server_time: u64,
|
server_time: u64,
|
||||||
phone_number_hint: String,
|
phone_number_hint: String,
|
||||||
|
confirm_type: AccountLinkConfirmType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountLinkSuccess {
|
impl AccountLinkSuccess {
|
||||||
|
@ -149,6 +168,28 @@ impl AccountLinkSuccess {
|
||||||
pub fn phone_number_hint(&self) -> &str {
|
pub fn phone_number_hint(&self) -> &str {
|
||||||
&self.phone_number_hint
|
&self.phone_number_hint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn confirm_type(&self) -> AccountLinkConfirmType {
|
||||||
|
self.confirm_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum AccountLinkConfirmType {
|
||||||
|
SMS = 1,
|
||||||
|
Email = 3,
|
||||||
|
Unknown(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for AccountLinkConfirmType {
|
||||||
|
fn from(i: i32) -> Self {
|
||||||
|
match i {
|
||||||
|
1 => AccountLinkConfirmType::SMS,
|
||||||
|
3 => AccountLinkConfirmType::Email,
|
||||||
|
_ => AccountLinkConfirmType::Unknown(i),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_device_id() -> String {
|
fn generate_device_id() -> String {
|
||||||
|
|
Loading…
Reference in a new issue