2021-03-24 17:49:09 -04:00
extern crate rpassword ;
2023-06-23 13:36:23 -04:00
use clap ::Parser ;
2021-03-27 10:35:52 -04:00
use log ::* ;
2021-08-01 12:43:18 +00:00
use std ::{
2023-06-23 13:36:23 -04:00
io ::Write ,
2021-08-08 12:54:46 -04:00
path ::Path ,
sync ::{ Arc , Mutex } ,
2021-08-01 12:43:18 +00:00
} ;
2023-06-22 16:20:15 -04:00
use steamguard ::protobufs ::steammessages_auth_steamclient ::{
EAuthSessionGuardType , EAuthTokenPlatformType ,
} ;
use steamguard ::token ::Tokens ;
2023-06-23 13:36:23 -04:00
use steamguard ::{ steamapi , DeviceDetails , LoginError , SteamGuardAccount , UserLogin } ;
2021-03-24 17:49:09 -04:00
2023-06-22 16:20:15 -04:00
use crate ::accountmanager ::migrate ::load_and_migrate ;
2023-06-23 13:36:23 -04:00
pub use crate ::accountmanager ::{ AccountManager , ManifestAccountLoadError , ManifestLoadError } ;
use crate ::commands ::{ CommandType , Subcommands } ;
2022-02-22 09:17:04 -05:00
2021-04-04 10:40:16 -04:00
extern crate lazy_static ;
2021-08-01 11:20:57 -04:00
#[ macro_use ]
extern crate anyhow ;
2021-08-15 11:52:54 -04:00
extern crate base64 ;
2021-08-12 18:06:18 -04:00
extern crate dirs ;
2021-08-20 09:37:55 -04:00
#[ cfg(test) ]
extern crate proptest ;
2021-08-15 11:52:54 -04:00
extern crate ring ;
2021-08-01 12:43:18 +00:00
mod accountmanager ;
2023-06-23 13:36:23 -04:00
mod commands ;
2023-06-24 08:17:56 -04:00
mod debug ;
2021-08-19 16:54:18 -04:00
mod encryption ;
2022-02-21 11:57:19 -05:00
mod errors ;
2023-06-22 16:20:15 -04:00
mod secret_string ;
2022-02-04 13:08:23 -05:00
pub ( crate ) mod tui ;
2021-04-04 10:40:16 -04:00
2021-08-17 18:12:49 -04:00
fn main ( ) {
2022-02-21 11:57:19 -05:00
std ::process ::exit ( match run ( ) {
Ok ( _ ) = > 0 ,
Err ( e ) = > {
error! ( " {:?} " , e ) ;
255
}
} ) ;
}
fn run ( ) -> anyhow ::Result < ( ) > {
2023-06-23 13:36:23 -04:00
let args = commands ::Args ::parse ( ) ;
2022-06-19 13:00:36 -04:00
info! ( " {:?} " , args ) ;
2021-03-27 09:31:38 -04:00
2023-06-23 13:36:23 -04:00
let globalargs = args . global ;
2021-08-08 12:54:46 -04:00
stderrlog ::new ( )
2023-06-23 13:36:23 -04:00
. verbosity ( globalargs . verbosity as usize )
2021-08-08 12:54:46 -04:00
. module ( module_path! ( ) )
2021-08-09 19:48:18 -04:00
. module ( " steamguard " )
2021-08-08 12:54:46 -04:00
. init ( )
. unwrap ( ) ;
2021-03-21 21:21:29 -04:00
2023-06-23 13:36:23 -04:00
let cmd : CommandType = match args . sub . unwrap_or ( Subcommands ::Code ( args . code ) ) {
Subcommands ::Debug ( args ) = > CommandType ::Const ( Box ::new ( args ) ) ,
Subcommands ::Completion ( args ) = > CommandType ::Const ( Box ::new ( args ) ) ,
Subcommands ::Setup ( args ) = > CommandType ::Manifest ( Box ::new ( args ) ) ,
Subcommands ::Import ( args ) = > CommandType ::Manifest ( Box ::new ( args ) ) ,
Subcommands ::Encrypt ( args ) = > CommandType ::Manifest ( Box ::new ( args ) ) ,
Subcommands ::Decrypt ( args ) = > CommandType ::Manifest ( Box ::new ( args ) ) ,
Subcommands ::Trade ( args ) = > CommandType ::Account ( Box ::new ( args ) ) ,
Subcommands ::Remove ( args ) = > CommandType ::Account ( Box ::new ( args ) ) ,
Subcommands ::Code ( args ) = > CommandType ::Account ( Box ::new ( args ) ) ,
#[ cfg(feature = " qr " ) ]
Subcommands ::Qr ( args ) = > CommandType ::Account ( Box ::new ( args ) ) ,
2022-06-19 10:48:18 -04:00
} ;
2021-07-29 19:42:45 -04:00
2023-06-23 13:36:23 -04:00
if let CommandType ::Const ( cmd ) = cmd {
return cmd . execute ( ) ;
}
let mafiles_dir = if let Some ( mafiles_path ) = & globalargs . mafiles_path {
2022-06-19 12:57:54 -04:00
mafiles_path . clone ( )
2021-08-12 18:36:03 -04:00
} else {
get_mafiles_dir ( )
} ;
2021-08-12 18:06:18 -04:00
info! ( " reading manifest from {} " , mafiles_dir ) ;
let path = Path ::new ( & mafiles_dir ) . join ( " manifest.json " ) ;
2023-06-22 16:20:15 -04:00
let mut manager : accountmanager ::AccountManager ;
2021-08-12 18:06:18 -04:00
if ! path . exists ( ) {
error! ( " Did not find manifest in {} " , mafiles_dir ) ;
2023-06-23 13:36:23 -04:00
if tui ::prompt_char (
2021-08-14 11:10:21 -04:00
format! ( " Would you like to create a manifest in {} ? " , mafiles_dir ) . as_str ( ) ,
" Yn " ,
2023-06-23 13:36:23 -04:00
) = = 'n'
{
info! ( " Aborting! " ) ;
return Err ( errors ::UserError ::Aborted . into ( ) ) ;
2021-08-08 12:54:46 -04:00
}
2022-02-21 11:57:19 -05:00
std ::fs ::create_dir_all ( mafiles_dir ) ? ;
2021-08-12 18:06:18 -04:00
2023-06-22 16:20:15 -04:00
manager = accountmanager ::AccountManager ::new ( path . as_path ( ) ) ;
manager . save ( ) ? ;
2021-08-12 18:54:38 -04:00
} else {
2023-06-22 16:20:15 -04:00
manager = match accountmanager ::AccountManager ::load ( path . as_path ( ) ) {
Ok ( m ) = > m ,
Err ( ManifestLoadError ::MigrationNeeded ) = > {
info! ( " Migrating manifest " ) ;
2023-06-23 13:36:23 -04:00
let ( manifest , accounts ) =
load_and_migrate ( path . as_path ( ) , globalargs . passkey . as_ref ( ) ) ? ;
2023-06-22 16:20:15 -04:00
let mut manager = AccountManager ::from_manifest ( manifest , mafiles_dir ) ;
manager . register_accounts ( accounts ) ;
2023-06-23 13:36:23 -04:00
manager . submit_passkey ( globalargs . passkey . clone ( ) ) ;
2023-06-22 16:20:15 -04:00
manager . save ( ) ? ;
manager
}
Err ( err ) = > {
error! ( " Failed to load manifest: {} " , err ) ;
return Err ( err . into ( ) ) ;
}
}
2021-08-08 12:54:46 -04:00
}
2021-03-27 10:35:52 -04:00
2023-06-23 13:36:23 -04:00
let mut passkey = globalargs . passkey . clone ( ) ;
2023-06-22 16:20:15 -04:00
manager . submit_passkey ( passkey ) ;
2021-08-15 23:20:49 -04:00
2021-08-16 21:13:58 -04:00
loop {
2023-06-22 16:20:15 -04:00
match manager . auto_upgrade ( ) {
2022-02-22 09:19:56 -05:00
Ok ( upgraded ) = > {
if upgraded {
info! ( " Manifest auto-upgraded " ) ;
2023-06-22 16:20:15 -04:00
manager . save ( ) ? ;
2022-06-12 22:01:35 -04:00
} else {
debug! ( " Manifest is up to date " ) ;
2022-02-22 09:19:56 -05:00
}
break ;
2022-02-22 09:38:41 -05:00
}
2021-08-16 21:13:58 -04:00
Err (
accountmanager ::ManifestAccountLoadError ::MissingPasskey
2021-08-20 10:01:23 -04:00
| accountmanager ::ManifestAccountLoadError ::IncorrectPasskey ,
2021-08-16 21:13:58 -04:00
) = > {
2023-06-22 16:20:15 -04:00
if manager . has_passkey ( ) {
2021-08-17 19:20:57 -04:00
error! ( " Incorrect passkey " ) ;
}
2022-09-08 16:16:20 -04:00
passkey = rpassword ::prompt_password_stderr ( " Enter encryption passkey: " ) . ok ( ) ;
2023-06-22 16:20:15 -04:00
manager . submit_passkey ( passkey ) ;
2021-08-16 21:13:58 -04:00
}
Err ( e ) = > {
error! ( " Could not load accounts: {} " , e ) ;
2022-02-21 11:57:19 -05:00
return Err ( e . into ( ) ) ;
2021-08-16 21:13:58 -04:00
}
}
}
2021-07-27 16:24:56 -04:00
2023-06-23 13:36:23 -04:00
if let CommandType ::Manifest ( cmd ) = cmd {
cmd . execute ( & mut manager ) ? ;
return Ok ( ( ) ) ;
2022-06-12 21:46:39 -04:00
}
2022-06-19 13:01:25 -04:00
let selected_accounts : Vec < Arc < Mutex < SteamGuardAccount > > > ;
2022-06-12 22:01:35 -04:00
loop {
2023-06-23 13:36:23 -04:00
match get_selected_accounts ( & globalargs , & mut manager ) {
2022-06-12 22:01:35 -04:00
Ok ( accounts ) = > {
selected_accounts = accounts ;
break ;
}
Err (
accountmanager ::ManifestAccountLoadError ::MissingPasskey
| accountmanager ::ManifestAccountLoadError ::IncorrectPasskey ,
) = > {
2023-06-22 16:20:15 -04:00
if manager . has_passkey ( ) {
2022-06-12 22:01:35 -04:00
error! ( " Incorrect passkey " ) ;
}
passkey = rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) . ok ( ) ;
2023-06-22 16:20:15 -04:00
manager . submit_passkey ( passkey ) ;
2022-06-12 22:01:35 -04:00
}
Err ( e ) = > {
error! ( " Could not load accounts: {} " , e ) ;
return Err ( e . into ( ) ) ;
}
}
}
2021-03-27 12:14:34 -04:00
2021-08-08 12:54:46 -04:00
debug! (
" selected accounts: {:?} " ,
selected_accounts
. iter ( )
. map ( | a | a . lock ( ) . unwrap ( ) . account_name . clone ( ) )
. collect ::< Vec < String > > ( )
) ;
2021-03-27 12:14:34 -04:00
2023-06-23 13:36:23 -04:00
if let CommandType ::Account ( cmd ) = cmd {
return cmd . execute ( & mut manager , selected_accounts ) ;
2021-08-08 12:54:46 -04:00
}
2023-06-23 13:36:23 -04:00
Ok ( ( ) )
2021-03-21 21:21:29 -04:00
}
2021-04-04 10:40:16 -04:00
2022-02-22 09:38:41 -05:00
fn get_selected_accounts (
2023-06-23 13:36:23 -04:00
args : & commands ::GlobalArgs ,
2023-06-22 16:20:15 -04:00
manifest : & mut accountmanager ::AccountManager ,
2022-02-22 09:38:41 -05:00
) -> anyhow ::Result < Vec < Arc < Mutex < SteamGuardAccount > > > , ManifestAccountLoadError > {
2022-02-22 09:17:04 -05:00
let mut selected_accounts : Vec < Arc < Mutex < SteamGuardAccount > > > = vec! [ ] ;
2022-06-19 12:57:54 -04:00
if args . all {
2022-02-22 09:17:04 -05:00
manifest . load_accounts ( ) ? ;
2023-06-22 16:20:15 -04:00
for entry in manifest . iter ( ) {
2022-02-22 09:17:04 -05:00
selected_accounts . push ( manifest . get_account ( & entry . account_name ) . unwrap ( ) . clone ( ) ) ;
}
} else {
2022-06-19 12:57:54 -04:00
let entry = if let Some ( username ) = & args . username {
2023-06-22 16:20:15 -04:00
manifest . get_entry ( username )
2022-02-22 09:17:04 -05:00
} else {
2022-02-22 09:38:41 -05:00
manifest
2023-06-22 16:20:15 -04:00
. iter ( )
. next ( )
2022-02-22 09:38:41 -05:00
. ok_or ( ManifestAccountLoadError ::MissingManifestEntry )
2022-02-22 09:17:04 -05:00
} ? ;
let account_name = entry . account_name . clone ( ) ;
let account = manifest . get_or_load_account ( & account_name ) ? ;
selected_accounts . push ( account ) ;
}
2023-06-22 16:20:15 -04:00
Ok ( selected_accounts )
2022-02-22 09:17:04 -05:00
}
2021-08-14 11:24:15 -04:00
fn do_login ( account : & mut SteamGuardAccount ) -> anyhow ::Result < ( ) > {
2023-06-22 16:20:15 -04:00
if ! account . account_name . is_empty ( ) {
2023-03-18 10:20:37 -04:00
info! ( " Username: {} " , account . account_name ) ;
2021-08-08 12:54:46 -04:00
} else {
2023-03-18 10:20:37 -04:00
eprint! ( " Username: " ) ;
2021-08-14 10:01:25 -04:00
account . account_name = tui ::prompt ( ) ;
2021-08-08 12:54:46 -04:00
}
let _ = std ::io ::stdout ( ) . flush ( ) ;
let password = rpassword ::prompt_password_stdout ( " Password: " ) . unwrap ( ) ;
2023-06-22 16:20:15 -04:00
if ! password . is_empty ( ) {
2021-08-08 12:54:46 -04:00
debug! ( " password is present " ) ;
} else {
debug! ( " password is empty " ) ;
}
2023-06-22 16:20:15 -04:00
let tokens = do_login_impl ( account . account_name . clone ( ) , password , Some ( account ) ) ? ;
let steam_id = tokens . access_token ( ) . decode ( ) ? . steam_id ( ) ;
account . set_tokens ( tokens ) ;
account . steam_id = steam_id ;
Ok ( ( ) )
2021-07-27 16:24:56 -04:00
}
2021-07-29 19:42:45 -04:00
2023-06-22 16:20:15 -04:00
fn do_login_raw ( username : String ) -> anyhow ::Result < Tokens > {
2021-08-08 18:32:50 -04:00
let _ = std ::io ::stdout ( ) . flush ( ) ;
let password = rpassword ::prompt_password_stdout ( " Password: " ) . unwrap ( ) ;
2023-06-22 16:20:15 -04:00
if ! password . is_empty ( ) {
2021-08-08 18:32:50 -04:00
debug! ( " password is present " ) ;
} else {
debug! ( " password is empty " ) ;
}
2023-06-22 16:20:15 -04:00
do_login_impl ( username , password , None )
2021-08-14 11:24:15 -04:00
}
fn do_login_impl (
username : String ,
password : String ,
account : Option < & SteamGuardAccount > ,
2023-06-22 16:20:15 -04:00
) -> anyhow ::Result < Tokens > {
2023-06-23 13:36:23 -04:00
let mut login = UserLogin ::new ( build_device_details ( ) ) ;
2023-06-22 16:20:15 -04:00
let mut password = password ;
2023-06-23 13:36:23 -04:00
let confirmation_methods ;
2021-08-08 18:32:50 -04:00
loop {
2023-06-22 16:20:15 -04:00
match login . begin_auth_via_credentials ( & username , & password ) {
Ok ( methods ) = > {
confirmation_methods = methods ;
break ;
2021-08-08 18:32:50 -04:00
}
2023-06-22 16:20:15 -04:00
Err ( LoginError ::TooManyAttempts ) = > {
error! ( " Too many login attempts. Steam is rate limiting you. Please wait a while and try again later. " ) ;
return Err ( LoginError ::TooManyAttempts . into ( ) ) ;
2021-08-08 18:32:50 -04:00
}
2023-06-22 16:20:15 -04:00
Err ( LoginError ::BadCredentials ) = > {
error! ( " Incorrect password. " ) ;
password = rpassword ::prompt_password_stdout ( " Password: " )
. unwrap ( )
. trim ( )
. to_owned ( ) ;
continue ;
}
Err ( err ) = > {
error! ( " Unexpected error when trying to log in. If you report this as a bug, please rerun with `-v debug` or `-v trace` and include all output in your issue. {:?} " , err ) ;
return Err ( err . into ( ) ) ;
2021-08-08 18:32:50 -04:00
}
}
2023-06-22 16:20:15 -04:00
}
2023-06-23 13:36:23 -04:00
for method in confirmation_methods {
2023-06-22 16:20:15 -04:00
match method . confirmation_type {
EAuthSessionGuardType ::k_EAuthSessionGuardType_DeviceConfirmation = > {
eprintln! ( " Please confirm this login on your other device. " ) ;
eprintln! ( " Press enter when you have confirmed. " ) ;
tui ::pause ( ) ;
}
EAuthSessionGuardType ::k_EAuthSessionGuardType_EmailConfirmation = > {
eprint! ( " Please confirm this login by clicking the link in your email. " ) ;
if ! method . associated_messsage . is_empty ( ) {
eprint! ( " ( {} ) " , method . associated_messsage ) ;
}
eprintln! ( ) ;
eprintln! ( " Press enter when you have confirmed. " ) ;
tui ::pause ( ) ;
}
EAuthSessionGuardType ::k_EAuthSessionGuardType_DeviceCode = > {
let code = if let Some ( account ) = account {
debug! ( " Generating 2fa code... " ) ;
2023-06-23 13:36:23 -04:00
let time = steamapi ::get_server_time ( ) ? . server_time ( ) ;
2023-06-22 16:20:15 -04:00
account . generate_code ( time )
} else {
eprint! ( " Enter the 2fa code from your device: " ) ;
tui ::prompt ( ) . trim ( ) . to_owned ( )
} ;
login . submit_steam_guard_code ( method . confirmation_type , code ) ? ;
}
EAuthSessionGuardType ::k_EAuthSessionGuardType_EmailCode = > {
eprint! ( " Enter the 2fa code sent to your email: " ) ;
let code = tui ::prompt ( ) . trim ( ) . to_owned ( ) ;
login . submit_steam_guard_code ( method . confirmation_type , code ) ? ;
}
_ = > {
warn! ( " Unknown confirmation method: {:?} " , method ) ;
continue ;
}
2021-08-08 18:32:50 -04:00
}
2023-06-22 16:20:15 -04:00
break ;
}
info! ( " Polling for tokens... -- If this takes a long time, try logging in again. " ) ;
let tokens = login . poll_until_tokens ( ) ? ;
info! ( " Logged in successfully! " ) ;
Ok ( tokens )
}
fn build_device_details ( ) -> DeviceDetails {
DeviceDetails {
friendly_name : format ! (
" {} (steamguard-cli) " ,
gethostname ::gethostname ( )
. into_string ( )
. expect ( " failed to get hostname " )
) ,
platform_type : EAuthTokenPlatformType ::k_EAuthTokenPlatformType_MobileApp ,
os_type : - 500 ,
gaming_device_type : 528 ,
2021-08-08 18:32:50 -04:00
}
}
2021-08-12 18:36:03 -04:00
fn get_mafiles_dir ( ) -> String {
2021-08-12 18:06:18 -04:00
let paths = vec! [
Path ::new ( & dirs ::config_dir ( ) . unwrap ( ) ) . join ( " steamguard-cli/maFiles " ) ,
Path ::new ( & dirs ::home_dir ( ) . unwrap ( ) ) . join ( " maFiles " ) ,
] ;
for path in & paths {
if path . join ( " manifest.json " ) . is_file ( ) {
return path . to_str ( ) . unwrap ( ) . into ( ) ;
}
}
return paths [ 0 ] . to_str ( ) . unwrap ( ) . into ( ) ;
}