2021-03-24 22:49:09 +01:00
extern crate rpassword ;
2023-06-23 19:36:23 +02:00
use clap ::Parser ;
2021-03-27 15:35:52 +01:00
use log ::* ;
2023-06-26 02:23:26 +02:00
use secrecy ::SecretString ;
2021-08-01 14:43:18 +02:00
use std ::{
2023-06-23 19:36:23 +02:00
io ::Write ,
2021-08-08 18:54:46 +02:00
path ::Path ,
sync ::{ Arc , Mutex } ,
2021-08-01 14:43:18 +02:00
} ;
2023-06-24 19:18:22 +02:00
use steamguard ::{
protobufs ::steammessages_auth_steamclient ::{ EAuthSessionGuardType , EAuthTokenPlatformType } ,
refresher ::TokenRefresher ,
transport ::WebApiTransport ,
2023-06-22 22:20:15 +02:00
} ;
2023-06-23 19:36:23 +02:00
use steamguard ::{ steamapi , DeviceDetails , LoginError , SteamGuardAccount , UserLogin } ;
2023-06-24 19:18:22 +02:00
use steamguard ::{ steamapi ::AuthenticationClient , token ::Tokens } ;
2021-03-24 22:49:09 +01:00
2023-06-27 02:15:22 +02:00
use crate ::accountmanager ::migrate ::{ load_and_migrate , MigrationError } ;
2023-06-23 19:36:23 +02:00
pub use crate ::accountmanager ::{ AccountManager , ManifestAccountLoadError , ManifestLoadError } ;
use crate ::commands ::{ CommandType , Subcommands } ;
2022-02-22 15:17:04 +01:00
2021-04-04 16:40:16 +02:00
extern crate lazy_static ;
2021-08-01 17:20:57 +02:00
#[ macro_use ]
extern crate anyhow ;
2021-08-15 17:52:54 +02:00
extern crate base64 ;
2021-08-13 00:06:18 +02:00
extern crate dirs ;
2021-08-20 15:37:55 +02:00
#[ cfg(test) ]
extern crate proptest ;
2021-08-15 17:52:54 +02:00
extern crate ring ;
2021-08-01 14:43:18 +02:00
mod accountmanager ;
2023-06-23 19:36:23 +02:00
mod commands ;
2023-06-24 14:17:56 +02:00
mod debug ;
2021-08-19 22:54:18 +02:00
mod encryption ;
2022-02-21 17:57:19 +01:00
mod errors ;
2023-06-22 22:20:15 +02:00
mod secret_string ;
2022-02-04 19:08:23 +01:00
pub ( crate ) mod tui ;
2023-06-25 15:24:53 +02:00
#[ cfg(feature = " updater " ) ]
mod updater ;
2021-04-04 16:40:16 +02:00
2021-08-18 00:12:49 +02:00
fn main ( ) {
2023-06-25 15:24:53 +02:00
let args = commands ::Args ::parse ( ) ;
stderrlog ::new ( )
. verbosity ( args . global . verbosity as usize )
. module ( module_path! ( ) )
. module ( " steamguard " )
. init ( )
. unwrap ( ) ;
debug! ( " {:?} " , args ) ;
#[ cfg(feature = " updater " ) ]
let should_do_update_check = ! args . global . no_update_check ;
let exit_code = match run ( args ) {
2022-02-21 17:57:19 +01:00
Ok ( _ ) = > 0 ,
Err ( e ) = > {
error! ( " {:?} " , e ) ;
255
}
2023-06-25 15:24:53 +02:00
} ;
2022-02-21 17:57:19 +01:00
2023-06-25 15:24:53 +02:00
#[ cfg(feature = " updater " ) ]
if should_do_update_check {
match updater ::check_for_update ( ) {
Ok ( Some ( version ) ) = > {
eprintln! ( ) ;
info! (
2023-06-29 23:16:02 +02:00
" steamguard-cli {} is available. Download it here: https://github.com/dyc3/steamguard-cli/releases " ,
2023-06-25 15:24:53 +02:00
version
) ;
}
Ok ( None ) = > {
debug! ( " No update available " ) ;
}
Err ( e ) = > {
warn! ( " Failed to check for updates: {} " , e ) ;
}
}
}
2021-03-27 14:31:38 +01:00
2023-06-25 15:24:53 +02:00
std ::process ::exit ( exit_code ) ;
}
2023-06-23 19:36:23 +02:00
2023-06-25 15:24:53 +02:00
fn run ( args : commands ::Args ) -> anyhow ::Result < ( ) > {
let globalargs = args . global ;
2021-03-22 02:21:29 +01:00
2023-06-23 19:36:23 +02: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 ) ) ,
2023-06-24 19:45:03 +02:00
Subcommands ::QrLogin ( args ) = > CommandType ::Account ( Box ::new ( args ) ) ,
2022-06-19 16:48:18 +02:00
} ;
2021-07-30 01:42:45 +02:00
2023-06-23 19:36:23 +02:00
if let CommandType ::Const ( cmd ) = cmd {
return cmd . execute ( ) ;
}
let mafiles_dir = if let Some ( mafiles_path ) = & globalargs . mafiles_path {
2022-06-19 18:57:54 +02:00
mafiles_path . clone ( )
2021-08-13 00:36:03 +02:00
} else {
get_mafiles_dir ( )
} ;
2021-08-13 00:06:18 +02:00
info! ( " reading manifest from {} " , mafiles_dir ) ;
let path = Path ::new ( & mafiles_dir ) . join ( " manifest.json " ) ;
2023-06-27 02:15:22 +02:00
let mut passkey = globalargs . passkey . clone ( ) ;
2023-06-22 22:20:15 +02:00
let mut manager : accountmanager ::AccountManager ;
2021-08-13 00:06:18 +02:00
if ! path . exists ( ) {
error! ( " Did not find manifest in {} " , mafiles_dir ) ;
2023-06-23 19:36:23 +02:00
if tui ::prompt_char (
2021-08-14 17:10:21 +02:00
format! ( " Would you like to create a manifest in {} ? " , mafiles_dir ) . as_str ( ) ,
" Yn " ,
2023-06-23 19:36:23 +02:00
) = = 'n'
{
info! ( " Aborting! " ) ;
return Err ( errors ::UserError ::Aborted . into ( ) ) ;
2021-08-08 18:54:46 +02:00
}
2022-02-21 17:57:19 +01:00
std ::fs ::create_dir_all ( mafiles_dir ) ? ;
2021-08-13 00:06:18 +02:00
2023-06-22 22:20:15 +02:00
manager = accountmanager ::AccountManager ::new ( path . as_path ( ) ) ;
manager . save ( ) ? ;
2021-08-13 00:54:38 +02:00
} else {
2023-06-22 22:20:15 +02:00
manager = match accountmanager ::AccountManager ::load ( path . as_path ( ) ) {
Ok ( m ) = > m ,
Err ( ManifestLoadError ::MigrationNeeded ) = > {
info! ( " Migrating manifest " ) ;
2023-06-27 02:15:22 +02:00
let manifest ;
let accounts ;
loop {
match load_and_migrate ( path . as_path ( ) , passkey . as_ref ( ) ) {
Ok ( ( m , a ) ) = > {
manifest = m ;
accounts = a ;
break ;
}
Err ( MigrationError ::MissingPasskey ) = > {
if passkey . is_some ( ) {
error! ( " Incorrect passkey " ) ;
}
let raw =
rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) ? ;
passkey = Some ( SecretString ::new ( raw ) ) ;
}
Err ( e ) = > {
error! ( " Failed to migrate manifest: {} " , e ) ;
return Err ( e . into ( ) ) ;
}
}
}
2023-06-22 22:20:15 +02:00
let mut manager = AccountManager ::from_manifest ( manifest , mafiles_dir ) ;
manager . register_accounts ( accounts ) ;
2023-06-23 19:36:23 +02:00
manager . submit_passkey ( globalargs . passkey . clone ( ) ) ;
2023-06-22 22:20:15 +02:00
manager . save ( ) ? ;
manager
}
Err ( err ) = > {
error! ( " Failed to load manifest: {} " , err ) ;
return Err ( err . into ( ) ) ;
}
}
2021-08-08 18:54:46 +02:00
}
2021-03-27 15:35:52 +01:00
2023-06-22 22:20:15 +02:00
manager . submit_passkey ( passkey ) ;
2021-08-16 05:20:49 +02:00
2021-08-17 03:13:58 +02:00
loop {
2023-06-22 22:20:15 +02:00
match manager . auto_upgrade ( ) {
2022-02-22 15:19:56 +01:00
Ok ( upgraded ) = > {
if upgraded {
info! ( " Manifest auto-upgraded " ) ;
2023-06-22 22:20:15 +02:00
manager . save ( ) ? ;
2022-06-13 04:01:35 +02:00
} else {
debug! ( " Manifest is up to date " ) ;
2022-02-22 15:19:56 +01:00
}
break ;
2022-02-22 15:38:41 +01:00
}
2021-08-17 03:13:58 +02:00
Err (
accountmanager ::ManifestAccountLoadError ::MissingPasskey
2021-08-20 16:01:23 +02:00
| accountmanager ::ManifestAccountLoadError ::IncorrectPasskey ,
2021-08-17 03:13:58 +02:00
) = > {
2023-06-22 22:20:15 +02:00
if manager . has_passkey ( ) {
2021-08-18 01:20:57 +02:00
error! ( " Incorrect passkey " ) ;
}
2023-06-26 02:23:26 +02:00
let raw = rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) ? ;
passkey = Some ( SecretString ::new ( raw ) ) ;
2023-06-22 22:20:15 +02:00
manager . submit_passkey ( passkey ) ;
2021-08-17 03:13:58 +02:00
}
Err ( e ) = > {
error! ( " Could not load accounts: {} " , e ) ;
2022-02-21 17:57:19 +01:00
return Err ( e . into ( ) ) ;
2021-08-17 03:13:58 +02:00
}
}
}
2021-07-27 22:24:56 +02:00
2023-06-23 19:36:23 +02:00
if let CommandType ::Manifest ( cmd ) = cmd {
cmd . execute ( & mut manager ) ? ;
return Ok ( ( ) ) ;
2022-06-13 03:46:39 +02:00
}
2022-06-19 19:01:25 +02:00
let selected_accounts : Vec < Arc < Mutex < SteamGuardAccount > > > ;
2022-06-13 04:01:35 +02:00
loop {
2023-06-23 19:36:23 +02:00
match get_selected_accounts ( & globalargs , & mut manager ) {
2022-06-13 04:01:35 +02:00
Ok ( accounts ) = > {
selected_accounts = accounts ;
break ;
}
Err (
accountmanager ::ManifestAccountLoadError ::MissingPasskey
| accountmanager ::ManifestAccountLoadError ::IncorrectPasskey ,
) = > {
2023-06-22 22:20:15 +02:00
if manager . has_passkey ( ) {
2022-06-13 04:01:35 +02:00
error! ( " Incorrect passkey " ) ;
}
2023-06-26 02:23:26 +02:00
let raw = rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) ? ;
passkey = Some ( SecretString ::new ( raw ) ) ;
2023-06-22 22:20:15 +02:00
manager . submit_passkey ( passkey ) ;
2022-06-13 04:01:35 +02:00
}
Err ( e ) = > {
error! ( " Could not load accounts: {} " , e ) ;
return Err ( e . into ( ) ) ;
}
}
}
2021-03-27 17:14:34 +01:00
2021-08-08 18:54:46 +02:00
debug! (
" selected accounts: {:?} " ,
selected_accounts
. iter ( )
. map ( | a | a . lock ( ) . unwrap ( ) . account_name . clone ( ) )
. collect ::< Vec < String > > ( )
) ;
2021-03-27 17:14:34 +01:00
2023-06-23 19:36:23 +02:00
if let CommandType ::Account ( cmd ) = cmd {
return cmd . execute ( & mut manager , selected_accounts ) ;
2021-08-08 18:54:46 +02:00
}
2023-06-23 19:36:23 +02:00
Ok ( ( ) )
2021-03-22 02:21:29 +01:00
}
2021-04-04 16:40:16 +02:00
2022-02-22 15:38:41 +01:00
fn get_selected_accounts (
2023-06-23 19:36:23 +02:00
args : & commands ::GlobalArgs ,
2023-06-22 22:20:15 +02:00
manifest : & mut accountmanager ::AccountManager ,
2022-02-22 15:38:41 +01:00
) -> anyhow ::Result < Vec < Arc < Mutex < SteamGuardAccount > > > , ManifestAccountLoadError > {
2022-02-22 15:17:04 +01:00
let mut selected_accounts : Vec < Arc < Mutex < SteamGuardAccount > > > = vec! [ ] ;
2022-06-19 18:57:54 +02:00
if args . all {
2022-02-22 15:17:04 +01:00
manifest . load_accounts ( ) ? ;
2023-06-22 22:20:15 +02:00
for entry in manifest . iter ( ) {
2022-02-22 15:17:04 +01:00
selected_accounts . push ( manifest . get_account ( & entry . account_name ) . unwrap ( ) . clone ( ) ) ;
}
} else {
2022-06-19 18:57:54 +02:00
let entry = if let Some ( username ) = & args . username {
2023-06-22 22:20:15 +02:00
manifest . get_entry ( username )
2022-02-22 15:17:04 +01:00
} else {
2022-02-22 15:38:41 +01:00
manifest
2023-06-22 22:20:15 +02:00
. iter ( )
. next ( )
2022-02-22 15:38:41 +01:00
. ok_or ( ManifestAccountLoadError ::MissingManifestEntry )
2022-02-22 15:17:04 +01:00
} ? ;
let account_name = entry . account_name . clone ( ) ;
let account = manifest . get_or_load_account ( & account_name ) ? ;
selected_accounts . push ( account ) ;
}
2023-06-22 22:20:15 +02:00
Ok ( selected_accounts )
2022-02-22 15:17:04 +01:00
}
2021-08-14 17:24:15 +02:00
fn do_login ( account : & mut SteamGuardAccount ) -> anyhow ::Result < ( ) > {
2023-06-24 19:18:22 +02:00
if let Some ( tokens ) = account . tokens . as_mut ( ) {
info! ( " Refreshing access token... " ) ;
2023-07-02 13:17:09 +02:00
let client = AuthenticationClient ::new ( WebApiTransport ::default ( ) ) ;
2023-06-24 19:18:22 +02:00
let mut refresher = TokenRefresher ::new ( client ) ;
match refresher . refresh ( account . steam_id , tokens ) {
Ok ( token ) = > {
info! ( " Successfully refreshed access token, no need to prompt to log in. " ) ;
tokens . set_access_token ( token ) ;
return Ok ( ( ) ) ;
}
Err ( err ) = > {
warn! (
" Failed to refresh access token, prompting for login: {} " ,
err
) ;
}
}
}
2023-06-22 22:20:15 +02:00
if ! account . account_name . is_empty ( ) {
2023-03-18 15:20:37 +01:00
info! ( " Username: {} " , account . account_name ) ;
2021-08-08 18:54:46 +02:00
} else {
2023-03-18 15:20:37 +01:00
eprint! ( " Username: " ) ;
2021-08-14 16:01:25 +02:00
account . account_name = tui ::prompt ( ) ;
2021-08-08 18:54:46 +02:00
}
let _ = std ::io ::stdout ( ) . flush ( ) ;
let password = rpassword ::prompt_password_stdout ( " Password: " ) . unwrap ( ) ;
2023-06-22 22:20:15 +02:00
if ! password . is_empty ( ) {
2021-08-08 18:54:46 +02:00
debug! ( " password is present " ) ;
} else {
debug! ( " password is empty " ) ;
}
2023-06-22 22:20:15 +02: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 22:24:56 +02:00
}
2021-07-30 01:42:45 +02:00
2023-06-22 22:20:15 +02:00
fn do_login_raw ( username : String ) -> anyhow ::Result < Tokens > {
2021-08-09 00:32:50 +02:00
let _ = std ::io ::stdout ( ) . flush ( ) ;
let password = rpassword ::prompt_password_stdout ( " Password: " ) . unwrap ( ) ;
2023-06-22 22:20:15 +02:00
if ! password . is_empty ( ) {
2021-08-09 00:32:50 +02:00
debug! ( " password is present " ) ;
} else {
debug! ( " password is empty " ) ;
}
2023-06-22 22:20:15 +02:00
do_login_impl ( username , password , None )
2021-08-14 17:24:15 +02:00
}
fn do_login_impl (
username : String ,
password : String ,
account : Option < & SteamGuardAccount > ,
2023-06-22 22:20:15 +02:00
) -> anyhow ::Result < Tokens > {
2023-07-02 13:17:09 +02:00
let mut login = UserLogin ::new ( WebApiTransport ::default ( ) , build_device_details ( ) ) ;
2023-06-22 22:20:15 +02:00
let mut password = password ;
2023-06-23 19:36:23 +02:00
let confirmation_methods ;
2021-08-09 00:32:50 +02:00
loop {
2023-06-22 22:20:15 +02:00
match login . begin_auth_via_credentials ( & username , & password ) {
Ok ( methods ) = > {
confirmation_methods = methods ;
break ;
2021-08-09 00:32:50 +02:00
}
2023-06-22 22:20:15 +02: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-09 00:32:50 +02:00
}
2023-06-22 22:20:15 +02: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-09 00:32:50 +02:00
}
}
2023-06-22 22:20:15 +02:00
}
2023-06-23 19:36:23 +02:00
for method in confirmation_methods {
2023-06-22 22:20:15 +02: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 19:36:23 +02:00
let time = steamapi ::get_server_time ( ) ? . server_time ( ) ;
2023-06-22 22:20:15 +02: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-09 00:32:50 +02:00
}
2023-06-22 22:20:15 +02: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-09 00:32:50 +02:00
}
}
2021-08-13 00:36:03 +02:00
fn get_mafiles_dir ( ) -> String {
2021-08-13 00:06:18 +02: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 ( ) ;
}