Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setup: add support for different link confirmation types, and verify that the authenticator was actually set up #348

Merged
merged 1 commit into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/accountmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion src/commands/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ where
}

for account_name in successful {
manager.remove_account(account_name);
manager.remove_account(&account_name);
}

manager.save()?;
Expand Down
49 changes: 40 additions & 9 deletions src/commands/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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())?;
}
Expand All @@ -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(_) => {}
Expand All @@ -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;
Expand All @@ -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(_) => {}
Expand Down
10 changes: 10 additions & 0 deletions src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 43 additions & 2 deletions steamguard/src/accountlinker.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
}
Expand All @@ -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);

Expand All @@ -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)?;

Expand All @@ -124,13 +127,29 @@ 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)]
pub struct AccountLinkSuccess {
account: SteamGuardAccount,
server_time: u64,
phone_number_hint: String,
confirm_type: AccountLinkConfirmType,
}

impl AccountLinkSuccess {
Expand All @@ -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 {
Expand Down
Loading