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") {
|
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![
|
||||||
|
|
|
@ -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(¶ms)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(¶ms)
|
||||||
|
.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 {}
|
||||||
|
|
Loading…
Reference in a new issue