implement the first step for account linking process

This commit is contained in:
Carson McManus 2021-08-08 18:32:50 -04:00
parent 2e4058cfca
commit 58897b6695
4 changed files with 195 additions and 47 deletions

View file

@ -120,13 +120,49 @@ fn main() {
} }
} }
manifest.load_accounts(); manifest
.load_accounts()
.expect("Failed to load accounts in manifest");
if matches.is_present("setup") { if matches.is_present("setup") {
info!("setup"); println!("Log in to the account that you want to link to steamguard-cli");
let mut linker = AccountLinker::new(); let session = do_login_raw().expect("Failed to log in. Account has not been linked.");
// do_login(&mut linker.account);
// linker.link(linker.account.session.expect("no login session")); let mut linker = AccountLinker::new(session);
let account: SteamGuardAccount;
loop {
match linker.link() {
Ok(a) => {
account = a;
break;
}
Err(err) => {
error!(
"Failed to link authenticator. Account has not been linked. {}",
err
);
return;
}
}
}
manifest.add_account(account);
match manifest.save() {
Ok(_) => {}
Err(err) => {
error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err);
println!(
"Just in case, here is the account info. Save it somewhere just in case!\n{:?}",
manifest.accounts.last().as_ref().unwrap()
);
return;
}
}
debug!("attempting link finalization");
print!("Enter SMS code: ");
let sms_code = prompt();
linker.finalize(sms_code);
return; return;
} }
@ -427,6 +463,51 @@ fn do_login(account: &mut SteamGuardAccount) {
} }
} }
fn do_login_raw() -> anyhow::Result<steamapi::Session> {
print!("Username: ");
let username = prompt();
let _ = std::io::stdout().flush();
let password = rpassword::prompt_password_stdout("Password: ").unwrap();
if password.len() > 0 {
debug!("password is present");
} else {
debug!("password is empty");
}
// TODO: reprompt if password is empty
let mut login = UserLogin::new(username, password);
let mut loops = 0;
loop {
match login.login() {
Ok(s) => {
return Ok(s);
}
Err(LoginError::Need2FA) => {
print!("Enter 2fa code: ");
let server_time = steamapi::get_server_time();
login.twofactor_code = prompt();
}
Err(LoginError::NeedCaptcha { captcha_gid }) => {
debug!("need captcha to log in");
login.captcha_text = prompt_captcha_text(&captcha_gid);
}
Err(LoginError::NeedEmail) => {
println!("You should have received an email with a code.");
print!("Enter code: ");
login.email_code = prompt();
}
Err(r) => {
error!("Fatal login result: {:?}", r);
bail!(r);
}
}
loops += 1;
if loops > 2 {
error!("Too many loops. Aborting login process, to avoid getting rate limited.");
bail!("Too many loops. Login process aborted to avoid getting rate limited.");
}
}
}
fn demo_confirmation_menu() { fn demo_confirmation_menu() {
info!("showing demo menu"); info!("showing demo menu");
let (accept, deny) = prompt_confirmation_menu(vec![ let (accept, deny) = prompt_confirmation_menu(vec![

View file

@ -1,56 +1,84 @@
use crate::{ use crate::{
steamapi::{AddAuthenticatorResponse, Session}, steamapi::{AddAuthenticatorResponse, Session, SteamApiClient},
SteamGuardAccount, SteamGuardAccount,
}; };
use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::fmt::Display; use std::fmt::Display;
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct AccountLinker { pub struct AccountLinker {
device_id: String, device_id: String,
phone_number: String, phone_number: String,
pub account: SteamGuardAccount, pub account: Option<SteamGuardAccount>,
pub finalized: bool, pub finalized: bool,
client: reqwest::blocking::Client, sent_confirmation_email: bool,
session: Session,
client: SteamApiClient,
} }
impl AccountLinker { impl AccountLinker {
pub fn new() -> AccountLinker { pub fn new(session: Session) -> AccountLinker {
return AccountLinker { return AccountLinker {
device_id: generate_device_id(), device_id: generate_device_id(),
phone_number: String::from(""), phone_number: "".into(),
account: SteamGuardAccount::new(), account: None,
finalized: false, finalized: false,
client: reqwest::blocking::ClientBuilder::new() sent_confirmation_email: false,
.cookie_store(true) session: session,
.build() client: SteamApiClient::new(),
.unwrap(),
}; };
} }
pub fn link( pub fn link(&mut self) -> anyhow::Result<SteamGuardAccount, AccountLinkError> {
&self, let has_phone = self.client.has_phone()?;
session: &mut Session,
) -> anyhow::Result<AddAuthenticatorResponse, AccountLinkError> {
let mut params = HashMap::new();
params.insert("access_token", session.token.clone());
params.insert("steamid", session.steam_id.to_string());
params.insert("device_identifier", self.device_id.clone());
params.insert("authenticator_type", "1".into());
params.insert("sms_phone_id", "1".into());
let resp: AddAuthenticatorResponse = self if has_phone && !self.phone_number.is_empty() {
.client return Err(AccountLinkError::MustRemovePhoneNumber);
.post("https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v0001") }
.form(&params) if !has_phone && self.phone_number.is_empty() {
.send()? return Err(AccountLinkError::MustProvidePhoneNumber);
.json()?; }
return Err(AccountLinkError::Unknown); if !has_phone {
if self.sent_confirmation_email {
if !self.client.check_email_confirmation()? {
return Err(AccountLinkError::Unknown(anyhow!(
"Failed email confirmation check"
)));
}
} else if !self.client.add_phone_number(self.phone_number.clone())? {
return Err(AccountLinkError::Unknown(anyhow!(
"Failed to add phone number"
)));
} else {
self.sent_confirmation_email = true;
return Err(AccountLinkError::MustConfirmEmail);
}
}
let resp: AddAuthenticatorResponse =
self.client.add_authenticator(self.device_id.clone())?;
match resp.response.status {
29 => {
return Err(AccountLinkError::AuthenticatorPresent);
}
1 => {
let mut account = resp.to_steam_guard_account();
account.device_id = self.device_id.clone();
account.session = self.client.session.clone();
return Ok(account);
}
status => {
return Err(AccountLinkError::Unknown(anyhow!(
"Unknown add authenticator status code: {}",
status
)));
}
}
} }
pub fn finalize(&self, session: &Session) {} pub fn finalize(&self, account: &SteamGuardAccount, sms_code: String) {}
} }
fn generate_device_id() -> String { fn generate_device_id() -> String {
@ -69,7 +97,7 @@ pub enum AccountLinkError {
AwaitingFinalization, AwaitingFinalization,
AuthenticatorPresent, AuthenticatorPresent,
NetworkFailure(reqwest::Error), NetworkFailure(reqwest::Error),
Unknown, Unknown(anyhow::Error),
} }
impl Display for AccountLinkError { impl Display for AccountLinkError {
@ -85,3 +113,9 @@ impl From<reqwest::Error> for AccountLinkError {
AccountLinkError::NetworkFailure(err) AccountLinkError::NetworkFailure(err)
} }
} }
impl From<anyhow::Error> for AccountLinkError {
fn from(err: anyhow::Error) -> AccountLinkError {
AccountLinkError::Unknown(err)
}
}

View file

@ -1,4 +1,4 @@
pub use accountlinker::{AccountLinkError, AccountLinker, AddAuthenticatorResponse}; pub use accountlinker::{AccountLinkError, AccountLinker};
use anyhow::Result; use anyhow::Result;
pub use confirmation::{Confirmation, ConfirmationType}; pub use confirmation::{Confirmation, ConfirmationType};
use hmacsha1::hmac_sha1; use hmacsha1::hmac_sha1;
@ -10,7 +10,8 @@ use reqwest::{
}; };
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, convert::TryInto, thread, time}; use std::{collections::HashMap, convert::TryInto};
pub use steamapi::AddAuthenticatorResponse;
pub use userlogin::{LoginError, UserLogin}; pub use userlogin::{LoginError, UserLogin};
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;

View file

@ -374,6 +374,36 @@ impl SteamApiClient {
Ok(resp) Ok(resp)
} }
///
/// Host: api.steampowered.com
/// Endpoint: POST /ITwoFactorService/FinalizeAddAuthenticator/v0001
pub fn finalize_authenticator(
&self,
sms_code: String,
code_2fa: String,
time_2fa: u64,
) -> anyhow::Result<FinalizeAddAuthenticatorResponse> {
ensure!(matches!(self.session, Some(_)));
let params = hashmap! {
"steamid" => self.session.as_ref().unwrap().steam_id.to_string(),
"access_token" => self.session.as_ref().unwrap().token.clone(),
"activation_code" => sms_code,
"authenticator_code" => code_2fa,
"authenticator_time" => time_2fa.to_string(),
};
let resp = self
.post(format!(
"{}/ITwoFactorService/FinalizeAddAuthenticator/v0001",
STEAM_API_BASE.to_string()
))
.form(&params)
.send()?
.json()?;
todo!();
}
} }
#[test] #[test]
@ -441,24 +471,26 @@ pub struct AddAuthenticatorResponseInner {
/// Spare shared secret /// Spare shared secret
pub secret_1: String, pub secret_1: String,
/// Result code /// Result code
pub status: String, pub status: i32,
} }
impl AddAuthenticatorResponse { impl AddAuthenticatorResponse {
pub fn to_steam_guard_account(&self) -> SteamGuardAccount { pub fn to_steam_guard_account(&self) -> SteamGuardAccount {
SteamGuardAccount { SteamGuardAccount {
shared_secret: self.response.shared_secret, shared_secret: self.response.shared_secret.clone(),
serial_number: self.response.serial_number, serial_number: self.response.serial_number.clone(),
revocation_code: self.response.revocation_code, revocation_code: self.response.revocation_code.clone(),
uri: self.response.uri, uri: self.response.uri.clone(),
server_time: self.response.server_time, server_time: self.response.server_time,
account_name: self.response.account_name, account_name: self.response.account_name.clone(),
token_gid: self.response.token_gid, token_gid: self.response.token_gid.clone(),
identity_secret: self.response.identity_secret, identity_secret: self.response.identity_secret.clone(),
secret_1: self.response.secret_1, secret_1: self.response.secret_1.clone(),
fully_enrolled: false, fully_enrolled: false,
device_id: "".into(), device_id: "".into(),
session: None, session: None,
} }
} }
} }
pub struct FinalizeAddAuthenticatorResponse {}