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(())
|
||||
}
|
||||
|
||||
pub fn remove_account(&mut self, account_name: String) {
|
||||
pub fn remove_account(&mut self, account_name: &String) {
|
||||
let index = self
|
||||
.manifest
|
||||
.entries
|
||||
.iter()
|
||||
.position(|a| a.account_name == account_name)
|
||||
.position(|a| &a.account_name == account_name)
|
||||
.unwrap();
|
||||
self.accounts.remove(&account_name);
|
||||
self.accounts.remove(account_name);
|
||||
self.manifest.entries.remove(index);
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ where
|
|||
}
|
||||
|
||||
for account_name in successful {
|
||||
manager.remove_account(account_name);
|
||||
manager.remove_account(&account_name);
|
||||
}
|
||||
|
||||
manager.save()?;
|
||||
|
|
|
@ -2,8 +2,11 @@ use log::*;
|
|||
use phonenumber::PhoneNumber;
|
||||
use secrecy::ExposeSecret;
|
||||
use steamguard::{
|
||||
accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient,
|
||||
token::Tokens, AccountLinkError, AccountLinker, FinalizeLinkError,
|
||||
accountlinker::{AccountLinkConfirmType, AccountLinkSuccess},
|
||||
phonelinker::PhoneLinker,
|
||||
steamapi::PhoneClient,
|
||||
token::Tokens,
|
||||
AccountLinkError, AccountLinker, FinalizeLinkError,
|
||||
};
|
||||
|
||||
use crate::{tui, AccountManager};
|
||||
|
@ -48,6 +51,7 @@ where
|
|||
break;
|
||||
}
|
||||
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.");
|
||||
do_add_phone_number(transport.clone(), linker.tokens())?;
|
||||
}
|
||||
|
@ -66,6 +70,7 @@ where
|
|||
}
|
||||
let mut server_time = link.server_time();
|
||||
let phone_number_hint = link.phone_number_hint().to_owned();
|
||||
let confirm_type = link.confirm_type();
|
||||
manager.add_account(link.into_account());
|
||||
match manager.save() {
|
||||
Ok(_) => {}
|
||||
|
@ -88,15 +93,29 @@ where
|
|||
tui::pause();
|
||||
|
||||
debug!("attempting link finalization");
|
||||
println!(
|
||||
"A code has been sent to your phone number ending in {}.",
|
||||
phone_number_hint
|
||||
);
|
||||
print!("Enter SMS code: ");
|
||||
let sms_code = tui::prompt();
|
||||
let confirm_code = match confirm_type {
|
||||
AccountLinkConfirmType::Email => {
|
||||
eprintln!(
|
||||
"A code has been sent to the email address associated with this account."
|
||||
);
|
||||
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;
|
||||
loop {
|
||||
match linker.finalize(server_time, &mut account, sms_code.clone()) {
|
||||
match linker.finalize(server_time, &mut account, confirm_code.clone()) {
|
||||
Ok(_) => break,
|
||||
Err(FinalizeLinkError::WantMore { server_time: s }) => {
|
||||
server_time = s;
|
||||
|
@ -116,6 +135,18 @@ where
|
|||
let revocation_code = account.revocation_code.clone();
|
||||
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.");
|
||||
match manager.save() {
|
||||
Ok(_) => {}
|
||||
|
|
10
src/tui.rs
10
src/tui.rs
|
@ -34,6 +34,16 @@ pub(crate) fn prompt() -> String {
|
|||
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.
|
||||
///
|
||||
/// `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::{
|
||||
CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request,
|
||||
CTwoFactor_Status_Request, CTwoFactor_Status_Response,
|
||||
};
|
||||
use crate::steamapi::twofactor::TwoFactorClient;
|
||||
use crate::token::TwoFactorSecret;
|
||||
|
@ -85,6 +86,7 @@ where
|
|||
account,
|
||||
server_time: resp.server_time(),
|
||||
phone_number_hint: resp.take_phone_number_hint(),
|
||||
confirm_type: resp.confirm_type().into(),
|
||||
};
|
||||
Ok(success)
|
||||
}
|
||||
|
@ -94,7 +96,7 @@ where
|
|||
&mut self,
|
||||
time: u64,
|
||||
account: &mut SteamGuardAccount,
|
||||
sms_code: String,
|
||||
confirm_code: String,
|
||||
) -> anyhow::Result<(), FinalizeLinkError> {
|
||||
let code = account.generate_code(time);
|
||||
|
||||
|
@ -105,7 +107,8 @@ where
|
|||
req.set_steamid(steam_id);
|
||||
req.set_authenticator_code(code);
|
||||
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)?;
|
||||
|
||||
|
@ -124,6 +127,21 @@ where
|
|||
self.finalized = true;
|
||||
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)]
|
||||
|
@ -131,6 +149,7 @@ pub struct AccountLinkSuccess {
|
|||
account: SteamGuardAccount,
|
||||
server_time: u64,
|
||||
phone_number_hint: String,
|
||||
confirm_type: AccountLinkConfirmType,
|
||||
}
|
||||
|
||||
impl AccountLinkSuccess {
|
||||
|
@ -149,6 +168,28 @@ impl AccountLinkSuccess {
|
|||
pub fn phone_number_hint(&self) -> &str {
|
||||
&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 {
|
||||
|
|
Loading…
Reference in a new issue