implement the first step for account linking process
This commit is contained in:
parent
2e4058cfca
commit
58897b6695
4 changed files with 195 additions and 47 deletions
91
src/main.rs
91
src/main.rs
|
@ -120,13 +120,49 @@ fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
manifest.load_accounts();
|
||||
manifest
|
||||
.load_accounts()
|
||||
.expect("Failed to load accounts in manifest");
|
||||
|
||||
if matches.is_present("setup") {
|
||||
info!("setup");
|
||||
let mut linker = AccountLinker::new();
|
||||
// do_login(&mut linker.account);
|
||||
// linker.link(linker.account.session.expect("no login session"));
|
||||
println!("Log in to the account that you want to link to steamguard-cli");
|
||||
let session = do_login_raw().expect("Failed to log in. Account has not been linked.");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
info!("showing demo menu");
|
||||
let (accept, deny) = prompt_confirmation_menu(vec![
|
||||
|
|
|
@ -1,56 +1,84 @@
|
|||
use crate::{
|
||||
steamapi::{AddAuthenticatorResponse, Session},
|
||||
steamapi::{AddAuthenticatorResponse, Session, SteamApiClient},
|
||||
SteamGuardAccount,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct AccountLinker {
|
||||
device_id: String,
|
||||
phone_number: String,
|
||||
pub account: SteamGuardAccount,
|
||||
pub account: Option<SteamGuardAccount>,
|
||||
pub finalized: bool,
|
||||
client: reqwest::blocking::Client,
|
||||
sent_confirmation_email: bool,
|
||||
session: Session,
|
||||
client: SteamApiClient,
|
||||
}
|
||||
|
||||
impl AccountLinker {
|
||||
pub fn new() -> AccountLinker {
|
||||
pub fn new(session: Session) -> AccountLinker {
|
||||
return AccountLinker {
|
||||
device_id: generate_device_id(),
|
||||
phone_number: String::from(""),
|
||||
account: SteamGuardAccount::new(),
|
||||
phone_number: "".into(),
|
||||
account: None,
|
||||
finalized: false,
|
||||
client: reqwest::blocking::ClientBuilder::new()
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
sent_confirmation_email: false,
|
||||
session: session,
|
||||
client: SteamApiClient::new(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn link(
|
||||
&self,
|
||||
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());
|
||||
pub fn link(&mut self) -> anyhow::Result<SteamGuardAccount, AccountLinkError> {
|
||||
let has_phone = self.client.has_phone()?;
|
||||
|
||||
let resp: AddAuthenticatorResponse = self
|
||||
.client
|
||||
.post("https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v0001")
|
||||
.form(¶ms)
|
||||
.send()?
|
||||
.json()?;
|
||||
if has_phone && !self.phone_number.is_empty() {
|
||||
return Err(AccountLinkError::MustRemovePhoneNumber);
|
||||
}
|
||||
if !has_phone && self.phone_number.is_empty() {
|
||||
return Err(AccountLinkError::MustProvidePhoneNumber);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -69,7 +97,7 @@ pub enum AccountLinkError {
|
|||
AwaitingFinalization,
|
||||
AuthenticatorPresent,
|
||||
NetworkFailure(reqwest::Error),
|
||||
Unknown,
|
||||
Unknown(anyhow::Error),
|
||||
}
|
||||
|
||||
impl Display for AccountLinkError {
|
||||
|
@ -85,3 +113,9 @@ impl From<reqwest::Error> for AccountLinkError {
|
|||
AccountLinkError::NetworkFailure(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for AccountLinkError {
|
||||
fn from(err: anyhow::Error) -> AccountLinkError {
|
||||
AccountLinkError::Unknown(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub use accountlinker::{AccountLinkError, AccountLinker, AddAuthenticatorResponse};
|
||||
pub use accountlinker::{AccountLinkError, AccountLinker};
|
||||
use anyhow::Result;
|
||||
pub use confirmation::{Confirmation, ConfirmationType};
|
||||
use hmacsha1::hmac_sha1;
|
||||
|
@ -10,7 +10,8 @@ use reqwest::{
|
|||
};
|
||||
use scraper::{Html, Selector};
|
||||
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};
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
|
|
@ -374,6 +374,36 @@ impl SteamApiClient {
|
|||
|
||||
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(¶ms)
|
||||
.send()?
|
||||
.json()?;
|
||||
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -441,24 +471,26 @@ pub struct AddAuthenticatorResponseInner {
|
|||
/// Spare shared secret
|
||||
pub secret_1: String,
|
||||
/// Result code
|
||||
pub status: String,
|
||||
pub status: i32,
|
||||
}
|
||||
|
||||
impl AddAuthenticatorResponse {
|
||||
pub fn to_steam_guard_account(&self) -> SteamGuardAccount {
|
||||
SteamGuardAccount {
|
||||
shared_secret: self.response.shared_secret,
|
||||
serial_number: self.response.serial_number,
|
||||
revocation_code: self.response.revocation_code,
|
||||
uri: self.response.uri,
|
||||
shared_secret: self.response.shared_secret.clone(),
|
||||
serial_number: self.response.serial_number.clone(),
|
||||
revocation_code: self.response.revocation_code.clone(),
|
||||
uri: self.response.uri.clone(),
|
||||
server_time: self.response.server_time,
|
||||
account_name: self.response.account_name,
|
||||
token_gid: self.response.token_gid,
|
||||
identity_secret: self.response.identity_secret,
|
||||
secret_1: self.response.secret_1,
|
||||
account_name: self.response.account_name.clone(),
|
||||
token_gid: self.response.token_gid.clone(),
|
||||
identity_secret: self.response.identity_secret.clone(),
|
||||
secret_1: self.response.secret_1.clone(),
|
||||
fully_enrolled: false,
|
||||
device_id: "".into(),
|
||||
session: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FinalizeAddAuthenticatorResponse {}
|
||||
|
|
Loading…
Reference in a new issue