2021-03-24 22:49:09 +01:00
extern crate rpassword ;
2022-06-13 03:46:39 +02:00
use clap ::{ crate_version , App , Arg , ArgMatches , Parser , Subcommand } ;
2021-03-27 15:35:52 +01:00
use log ::* ;
2021-08-18 00:12:49 +02:00
use std ::str ::FromStr ;
2021-08-01 14:43:18 +02:00
use std ::{
2021-08-14 16:01:25 +02:00
io ::{ stdout , Write } ,
2021-08-08 18:54:46 +02:00
path ::Path ,
sync ::{ Arc , Mutex } ,
2021-08-01 14:43:18 +02:00
} ;
2021-08-08 18:34:06 +02:00
use steamguard ::{
2021-08-14 16:01:25 +02:00
steamapi , AccountLinkError , AccountLinker , Confirmation , FinalizeLinkError , LoginError ,
SteamGuardAccount , UserLogin ,
2021-08-01 14:43:18 +02:00
} ;
2021-03-24 22:49:09 +01:00
2022-02-22 15:17:04 +01:00
use crate ::accountmanager ::ManifestAccountLoadError ;
2021-04-04 16:40:16 +02:00
#[ macro_use ]
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 ;
2021-08-14 16:01:25 +02:00
mod demos ;
2021-08-19 22:54:18 +02:00
mod encryption ;
2022-02-21 17:57:19 +01:00
mod errors ;
2021-08-14 16:01:25 +02:00
mod tui ;
2021-04-04 16:40:16 +02:00
2022-06-12 18:17:26 +02:00
#[ derive(Debug, Clone, Parser) ]
2022-06-12 18:38:54 +02:00
#[ clap(author, version, about = " Generate Steam 2FA codes and confirm Steam trades from the command line. " , long_about = None) ]
2022-06-12 18:17:26 +02:00
struct Args {
#[ clap(short, long, help = " Steam username, case-sensitive. " , long_help = " Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected. " ) ]
username : Option < String > ,
#[ clap(short, long, help = " Select all accounts in the manifest. " ) ]
all : bool ,
/// The path to the maFiles directory.
#[ clap(short, long, default_value = " ~/.config/steamguard-cli/maFiles " , help = " Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. " ) ]
mafiles_path : String ,
#[ clap(short, long, help = " Specify your encryption passkey. " ) ]
passkey : Option < String > ,
#[ clap(short, long, default_value_t=Verbosity::Info, help = " Set the log level. " ) ]
verbosity : Verbosity ,
2022-06-12 18:38:54 +02:00
#[ clap(subcommand) ]
2022-06-13 03:46:39 +02:00
sub : Option < Subcommands > ,
2022-06-12 18:38:54 +02:00
}
#[ derive(Debug, Clone, Parser) ]
enum Subcommands {
Debug {
#[ clap(long) ]
demo_conf_menu : bool
} ,
// Completions {
// TODO: Add completions
// },
#[ clap(about = " Interactive interface for trade confirmations " ) ]
Trade {
#[ clap(short, long, help = " Accept all open trade confirmations. Does not open interactive interface. " ) ]
accept_all : bool ,
#[ clap(short, long, help = " If submitting a confirmation response fails, exit immediately. " ) ]
fail_fast : bool ,
} ,
#[ clap(about = " Set up a new account with steamguard-cli " ) ]
Setup {
#[ clap(short, long, from_global, help = " Steam username, case-sensitive. " ) ]
2022-06-13 03:46:39 +02:00
username : Option < String > ,
2022-06-12 18:38:54 +02:00
} ,
#[ clap(about = " Import an account with steamguard already set up " ) ]
Import {
#[ clap(long, help = " Paths to one or more maFiles, eg. \" ./gaben.maFile \" " ) ]
files : Vec < String > ,
} ,
#[ clap(about = " Remove the authenticator from an account. " ) ]
Remove {
#[ clap(short, long, from_global, help = " Steam username, case-sensitive. " ) ]
username : String ,
} ,
#[ clap(about = " Encrypt all maFiles " ) ]
Encrypt ,
#[ clap(about = " Decrypt all maFiles " ) ]
Decrypt ,
2022-06-12 18:17:26 +02:00
}
#[ derive(Debug, Clone, Copy) ]
enum Verbosity {
Error = 0 ,
Warn = 1 ,
Info = 2 ,
Debug = 3 ,
Trace = 4 ,
}
impl std ::fmt ::Display for Verbosity {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
f . write_fmt ( format_args! ( " {} " , match self {
Verbosity ::Error = > " error " ,
Verbosity ::Warn = > " warn " ,
Verbosity ::Info = > " info " ,
Verbosity ::Debug = > " debug " ,
Verbosity ::Trace = > " trace " ,
} ) )
}
}
impl FromStr for Verbosity {
type Err = anyhow ::Error ;
fn from_str ( s : & str ) -> Result < Self , Self ::Err > {
match s {
" error " = > Ok ( Verbosity ::Error ) ,
" warn " = > Ok ( Verbosity ::Warn ) ,
" info " = > Ok ( Verbosity ::Info ) ,
" debug " = > Ok ( Verbosity ::Debug ) ,
" trace " = > Ok ( Verbosity ::Trace ) ,
_ = > Err ( anyhow! ( " Invalid verbosity level: {} " , s ) ) ,
}
}
}
2022-06-19 16:38:59 +02:00
struct ArgsSetup {
username : Option < String > ,
}
impl From < Subcommands > for ArgsSetup {
fn from ( sub : Subcommands ) -> Self {
match sub {
Subcommands ::Setup { username } = > Self { username } ,
_ = > panic! ( " ArgsSetup::from() called with non-Setup subcommand " ) ,
}
}
}
struct ArgsImport {
files : Vec < String > ,
}
impl From < Subcommands > for ArgsImport {
fn from ( sub : Subcommands ) -> Self {
match sub {
Subcommands ::Import { files } = > Self { files } ,
_ = > panic! ( " ArgsImport::from() called with non-Import subcommand " ) ,
}
}
}
struct ArgsTrade {
accept_all : bool ,
fail_fast : bool ,
}
impl From < Subcommands > for ArgsTrade {
fn from ( sub : Subcommands ) -> Self {
match sub {
Subcommands ::Trade { accept_all , fail_fast } = > Self { accept_all , fail_fast } ,
_ = > panic! ( " ArgsTrade::from() called with non-Trade subcommand " ) ,
}
}
}
2022-06-12 18:17:26 +02:00
fn cli ( ) -> App < 'static > {
2021-08-18 00:12:49 +02:00
App ::new ( " steamguard-cli " )
2021-03-27 14:31:38 +01:00
. version ( crate_version! ( ) )
. bin_name ( " steamguard " )
. author ( " dyc3 (Carson McManus) " )
. about ( " Generate Steam 2FA codes and confirm Steam trades from the command line. " )
. arg (
Arg ::with_name ( " username " )
. long ( " username " )
2022-06-12 18:17:26 +02:00
. short ( 'u' )
2021-08-10 04:25:39 +02:00
. takes_value ( true )
2021-09-01 14:56:24 +02:00
. help ( " Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected. " )
2021-08-12 01:58:18 +02:00
. conflicts_with ( " all " )
2021-03-27 14:31:38 +01:00
)
. arg (
Arg ::with_name ( " all " )
. long ( " all " )
2022-06-12 18:17:26 +02:00
. short ( 'a' )
2021-03-27 17:14:34 +01:00
. takes_value ( false )
2021-03-27 14:31:38 +01:00
. help ( " Select all accounts in the manifest. " )
2021-08-12 01:58:18 +02:00
. conflicts_with ( " username " )
2021-03-27 14:31:38 +01:00
)
. arg (
Arg ::with_name ( " mafiles-path " )
. long ( " mafiles-path " )
2022-06-12 18:17:26 +02:00
. short ( 'm' )
2021-03-27 14:31:38 +01:00
. default_value ( " ~/maFiles " )
2021-08-13 00:06:18 +02:00
. help ( " Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. " )
2021-03-27 14:31:38 +01:00
)
. arg (
Arg ::with_name ( " passkey " )
. long ( " passkey " )
2022-06-12 18:17:26 +02:00
. short ( 'p' )
2021-03-27 14:31:38 +01:00
. help ( " Specify your encryption passkey. " )
2021-08-18 01:04:02 +02:00
. takes_value ( true )
2021-03-27 14:31:38 +01:00
)
2021-03-27 15:35:52 +01:00
. arg (
Arg ::with_name ( " verbosity " )
2022-06-12 18:17:26 +02:00
. short ( 'v' )
2021-03-27 15:35:52 +01:00
. help ( " Log what is going on verbosely. " )
. takes_value ( false )
. multiple ( true )
)
2022-06-12 18:17:26 +02:00
// .subcommand(
// App::new("completion")
// .about("Generate shell completions")
// .arg(
// Arg::with_name("shell")
// .long("shell")
// .takes_value(true)
// .possible_values(&Shell::variants())
// )
// )
2021-03-27 14:31:38 +01:00
. subcommand (
App ::new ( " trade " )
. about ( " Interactive interface for trade confirmations " )
. arg (
Arg ::with_name ( " accept-all " )
2022-06-12 18:17:26 +02:00
. short ( 'a' )
2022-02-21 18:14:15 +01:00
. long ( " accept-all " )
. takes_value ( false )
. help ( " Accept all open trade confirmations. Does not open interactive interface. " )
)
. arg (
Arg ::with_name ( " fail-fast " )
. takes_value ( false )
. help ( " If submitting a confirmation response fails, exit immediately. " )
2021-03-27 14:31:38 +01:00
)
)
2021-07-27 22:24:56 +02:00
. subcommand (
App ::new ( " setup " )
. about ( " Set up a new account with steamguard-cli " )
)
2021-08-14 01:04:03 +02:00
. subcommand (
App ::new ( " import " )
. about ( " Import an account with steamguard already set up " )
. arg (
Arg ::with_name ( " files " )
. required ( true )
. multiple ( true )
)
)
2021-08-12 01:39:29 +02:00
. subcommand (
App ::new ( " remove " )
. about ( " Remove the authenticator from an account. " )
)
2021-08-16 05:20:49 +02:00
. subcommand (
App ::new ( " encrypt " )
. about ( " Encrypt maFiles. " )
)
. subcommand (
App ::new ( " decrypt " )
. about ( " Decrypt maFiles. " )
)
2021-07-30 01:42:45 +02:00
. subcommand (
App ::new ( " debug " )
. arg (
Arg ::with_name ( " demo-conf-menu " )
. help ( " Show an example confirmation menu using dummy data. " )
. takes_value ( false )
)
)
2021-08-18 00:12:49 +02:00
}
fn main ( ) {
2022-02-21 17:57:19 +01:00
std ::process ::exit ( match run ( ) {
Ok ( _ ) = > 0 ,
Err ( e ) = > {
error! ( " {:?} " , e ) ;
255
}
} ) ;
}
fn run ( ) -> anyhow ::Result < ( ) > {
2022-06-12 18:17:26 +02:00
let new_args = Args ::parse ( ) ;
println! ( " {:?} " , new_args ) ;
2021-08-18 00:12:49 +02:00
let matches = cli ( ) . get_matches ( ) ;
2021-03-27 14:31:38 +01:00
2021-08-08 18:54:46 +02:00
stderrlog ::new ( )
2022-06-12 18:17:26 +02:00
. verbosity ( new_args . verbosity as usize )
2021-08-08 18:54:46 +02:00
. module ( module_path! ( ) )
2021-08-10 01:48:18 +02:00
. module ( " steamguard " )
2021-08-08 18:54:46 +02:00
. init ( )
. unwrap ( ) ;
2021-03-22 02:21:29 +01:00
2022-06-13 03:46:39 +02:00
if let Some ( subcmd ) = new_args . sub {
match subcmd {
Subcommand ::Debug { demo_conf_menu } = > {
if demo_conf_menu {
demos ::demo_confirmation_menu ( ) ;
}
return Ok ( ( ) ) ;
} ,
// Subcommand::Completions{shell} => {
// // cli().gen_completions_to(
// // "steamguard",
// // Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(),
// // &mut std::io::stdout(),
// // );
// return Ok(());
// },
_ = > { } ,
} ;
2021-08-18 00:12:49 +02:00
}
2021-07-30 01:42:45 +02:00
2021-08-13 00:36:03 +02:00
let mafiles_dir = if matches . occurrences_of ( " mafiles-path " ) > 0 {
matches . value_of ( " mafiles-path " ) . unwrap ( ) . into ( )
} 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 " ) ;
2021-08-08 18:54:46 +02:00
let mut manifest : accountmanager ::Manifest ;
2021-08-13 00:06:18 +02:00
if ! path . exists ( ) {
error! ( " Did not find manifest in {} " , mafiles_dir ) ;
2021-08-14 17:10:21 +02:00
match tui ::prompt_char (
format! ( " Would you like to create a manifest in {} ? " , mafiles_dir ) . as_str ( ) ,
" Yn " ,
) {
'n' = > {
2021-08-13 00:06:18 +02:00
info! ( " Aborting! " ) ;
2022-02-21 17:57:19 +01:00
return Err ( errors ::UserError ::Aborted . into ( ) ) ;
2021-08-13 00:06:18 +02:00
}
_ = > { }
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
manifest = accountmanager ::Manifest ::new ( path . as_path ( ) ) ;
2022-02-18 20:55:10 +01:00
manifest . save ( ) ? ;
2021-08-13 00:54:38 +02:00
} else {
2022-02-21 17:57:19 +01:00
manifest = accountmanager ::Manifest ::load ( path . as_path ( ) ) ? ;
2021-08-08 18:54:46 +02:00
}
2021-03-27 15:35:52 +01:00
2021-08-17 03:13:58 +02:00
let mut passkey : Option < String > = matches . value_of ( " passkey " ) . map ( | s | s . into ( ) ) ;
2022-02-18 20:55:10 +01:00
manifest . submit_passkey ( passkey ) ;
2021-08-16 05:20:49 +02:00
2021-08-17 03:13:58 +02:00
loop {
2022-02-22 15:19:56 +01:00
match manifest . auto_upgrade ( ) {
Ok ( upgraded ) = > {
if upgraded {
info! ( " Manifest auto-upgraded " ) ;
manifest . 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
) = > {
2022-02-18 20:55:10 +01:00
if manifest . has_passkey ( ) {
2021-08-18 01:20:57 +02:00
error! ( " Incorrect passkey " ) ;
}
2021-08-17 03:13:58 +02:00
passkey = rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) . ok ( ) ;
2022-02-18 20:55:10 +01:00
manifest . 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
2022-06-13 03:46:39 +02:00
if let Some ( subcmd ) = new_args . sub {
match subcmd {
2022-06-19 16:38:59 +02:00
Subcommands ::Setup { username } = > {
do_subcmd_setup ( new_args . sub . unwrap ( ) . into ( ) , & mut manifest ) ? ;
2022-06-13 03:46:39 +02:00
} ,
2022-06-19 16:38:59 +02:00
Subcommands ::Import { files } = > { todo! ( ) } ,
Subcommands ::Encrypt { } = > { todo! ( ) } ,
Subcommands ::Decrypt { } = > { todo! ( ) } ,
_ = > { } ,
2022-06-13 03:46:39 +02:00
}
}
2021-08-08 18:54:46 +02:00
if matches . is_present ( " setup " ) {
2021-08-09 00:32:50 +02:00
2021-08-14 01:04:03 +02:00
} else if let Some ( import_matches ) = matches . subcommand_matches ( " import " ) {
for file_path in import_matches . values_of ( " files " ) . unwrap ( ) {
match manifest . import_account ( file_path . into ( ) ) {
Ok ( _ ) = > {
info! ( " Imported account: {} " , file_path ) ;
}
Err ( err ) = > {
2022-02-21 17:57:19 +01:00
bail! ( " Failed to import account: {} {} " , file_path , err ) ;
2021-08-14 01:04:03 +02:00
}
}
}
2022-02-18 20:55:10 +01:00
manifest . save ( ) ? ;
2022-02-21 17:57:19 +01:00
return Ok ( ( ) ) ;
2021-08-16 05:20:49 +02:00
} else if matches . is_present ( " encrypt " ) {
2022-02-18 20:55:10 +01:00
if ! manifest . has_passkey ( ) {
2021-08-18 01:20:57 +02:00
loop {
passkey = rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) . ok ( ) ;
2021-08-19 18:54:52 +02:00
let passkey_confirm =
rpassword ::prompt_password_stdout ( " Confirm encryption passkey: " ) . ok ( ) ;
2021-08-18 01:20:57 +02:00
if passkey = = passkey_confirm {
break ;
}
error! ( " Passkeys do not match, try again. " ) ;
}
2022-02-18 20:55:10 +01:00
manifest . submit_passkey ( passkey ) ;
2021-08-18 01:20:57 +02:00
}
2022-02-22 15:29:34 +01:00
manifest . load_accounts ( ) ? ;
2021-08-16 05:20:49 +02:00
for entry in & mut manifest . entries {
entry . encryption = Some ( accountmanager ::EntryEncryptionParams ::generate ( ) ) ;
}
2022-02-18 20:55:10 +01:00
manifest . save ( ) ? ;
2022-02-21 17:57:19 +01:00
return Ok ( ( ) ) ;
2021-08-18 00:54:16 +02:00
} else if matches . is_present ( " decrypt " ) {
2022-02-22 15:29:34 +01:00
manifest . load_accounts ( ) ? ;
2021-08-18 00:54:16 +02:00
for entry in & mut manifest . entries {
entry . encryption = None ;
}
2022-02-18 20:55:10 +01:00
manifest . submit_passkey ( None ) ;
manifest . save ( ) ? ;
2022-02-21 17:57:19 +01:00
return Ok ( ( ) ) ;
2021-08-08 18:54:46 +02:00
}
2021-07-27 22:24:56 +02:00
2022-06-13 04:01:35 +02:00
let mut selected_accounts : Vec < Arc < Mutex < SteamGuardAccount > > > ;
loop {
match get_selected_accounts ( & matches , & mut manifest ) {
Ok ( accounts ) = > {
selected_accounts = accounts ;
break ;
}
Err (
accountmanager ::ManifestAccountLoadError ::MissingPasskey
| accountmanager ::ManifestAccountLoadError ::IncorrectPasskey ,
) = > {
if manifest . has_passkey ( ) {
error! ( " Incorrect passkey " ) ;
}
passkey = rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) . ok ( ) ;
manifest . submit_passkey ( passkey ) ;
}
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
2022-06-13 03:46:39 +02:00
if let Some ( subcmd ) = new_args . sub {
match subcmd {
Subcommands ::Trade { accept_all , fail_fast } = > {
2022-06-19 16:38:59 +02:00
for a in selected_accounts . iter_mut ( ) {
let mut account = a . lock ( ) . unwrap ( ) ;
info! ( " Checking for trade confirmations " ) ;
let confirmations : Vec < Confirmation > ;
loop {
match account . get_trade_confirmations ( ) {
Ok ( confs ) = > {
confirmations = confs ;
break ;
}
Err ( _ ) = > {
info! ( " failed to get trade confirmations, asking user to log in " ) ;
do_login ( & mut account ) ? ;
}
}
2021-08-08 18:54:46 +02:00
}
2021-07-29 15:08:06 +02:00
2022-06-19 16:38:59 +02:00
let mut any_failed = false ;
if accept_all {
info! ( " accepting all confirmations " ) ;
for conf in & confirmations {
let result = account . accept_confirmation ( conf ) ;
if result . is_err ( ) {
warn! ( " accept confirmation result: {:?} " , result ) ;
any_failed = true ;
if fail_fast {
return result ;
}
} else {
debug! ( " accept confirmation result: {:?} " , result ) ;
}
2022-02-21 18:14:15 +01:00
}
} else {
2022-06-19 16:38:59 +02:00
if termion ::is_tty ( & stdout ( ) ) {
let ( accept , deny ) = tui ::prompt_confirmation_menu ( confirmations ) ;
for conf in & accept {
let result = account . accept_confirmation ( conf ) ;
if result . is_err ( ) {
warn! ( " accept confirmation result: {:?} " , result ) ;
any_failed = true ;
if fail_fast {
return result ;
}
} else {
debug! ( " accept confirmation result: {:?} " , result ) ;
}
2022-02-21 18:14:15 +01:00
}
2022-06-19 16:38:59 +02:00
for conf in & deny {
let result = account . deny_confirmation ( conf ) ;
debug! ( " deny confirmation result: {:?} " , result ) ;
if result . is_err ( ) {
warn! ( " deny confirmation result: {:?} " , result ) ;
any_failed = true ;
if fail_fast {
return result ;
}
} else {
debug! ( " deny confirmation result: {:?} " , result ) ;
}
2022-02-21 18:14:15 +01:00
}
} else {
2022-06-19 16:38:59 +02:00
warn! ( " not a tty, not showing menu " ) ;
for conf in & confirmations {
println! ( " {} " , conf . description ( ) ) ;
}
2022-02-21 18:14:15 +01:00
}
2021-08-08 18:54:46 +02:00
}
2022-06-19 16:38:59 +02:00
if any_failed {
error! ( " Failed to respond to some confirmations. " ) ;
2021-08-08 18:54:46 +02:00
}
}
2021-08-01 17:20:57 +02:00
2022-06-19 16:38:59 +02:00
manifest . save ( ) ? ;
} ,
Subcommands ::Remove { username } = > {
println! (
" This will remove the mobile authenticator from {} accounts: {} " ,
selected_accounts . len ( ) ,
selected_accounts
. iter ( )
. map ( | a | a . lock ( ) . unwrap ( ) . account_name . clone ( ) )
. collect ::< Vec < String > > ( )
. join ( " , " )
) ;
2021-08-12 01:39:29 +02:00
2022-06-19 16:38:59 +02:00
match tui ::prompt_char ( " Do you want to continue? " , " yN " ) {
'y' = > { }
_ = > {
info! ( " Aborting! " ) ;
return Err ( errors ::UserError ::Aborted . into ( ) ) ;
}
}
2021-08-12 01:39:29 +02:00
2022-06-19 16:38:59 +02:00
let mut successful = vec! [ ] ;
for a in selected_accounts {
let account = a . lock ( ) . unwrap ( ) ;
match account . remove_authenticator ( None ) {
Ok ( success ) = > {
if success {
println! ( " Removed authenticator from {} " , account . account_name ) ;
2021-09-06 22:14:57 +02:00
successful . push ( account . account_name . clone ( ) ) ;
2022-06-19 16:38:59 +02:00
} else {
println! (
" Failed to remove authenticator from {} " ,
account . account_name
) ;
match tui ::prompt_char (
" Would you like to remove it from the manifest anyway? " ,
" yN " ,
) {
'y' = > {
successful . push ( account . account_name . clone ( ) ) ;
}
_ = > { }
}
2021-09-06 22:14:57 +02:00
}
2022-06-19 16:38:59 +02:00
}
Err ( err ) = > {
error! (
" Unexpected error when removing authenticator from {}: {} " ,
account . account_name , err
) ;
2021-09-06 22:14:57 +02:00
}
2021-08-12 01:39:29 +02:00
}
}
2022-06-19 16:38:59 +02:00
for account_name in successful {
manifest . remove_account ( account_name ) ;
2021-08-12 01:39:29 +02:00
}
2022-06-19 16:38:59 +02:00
manifest . save ( ) ? ;
} ,
s = > {
error! ( " Unknown subcommand: {:?} " , s ) ;
} ,
}
} else {
let server_time = steamapi ::get_server_time ( ) ;
debug! ( " Time used to generate codes: {} " , server_time ) ;
for account in selected_accounts {
info! (
" Generating code for {} " ,
account . lock ( ) . unwrap ( ) . account_name
) ;
trace! ( " {:?} " , account ) ;
let code = account . lock ( ) . unwrap ( ) . generate_code ( server_time ) ;
println! ( " {} " , code ) ;
2021-08-12 01:39:29 +02:00
}
2021-08-08 18:54:46 +02:00
}
2022-06-19 16:38:59 +02:00
2022-02-21 17:57:19 +01: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 (
matches : & ArgMatches ,
manifest : & mut accountmanager ::Manifest ,
) -> anyhow ::Result < Vec < Arc < Mutex < SteamGuardAccount > > > , ManifestAccountLoadError > {
2022-02-22 15:17:04 +01:00
let mut selected_accounts : Vec < Arc < Mutex < SteamGuardAccount > > > = vec! [ ] ;
if matches . is_present ( " all " ) {
manifest . load_accounts ( ) ? ;
for entry in & manifest . entries {
selected_accounts . push ( manifest . get_account ( & entry . account_name ) . unwrap ( ) . clone ( ) ) ;
}
} else {
let entry = if matches . is_present ( " username " ) {
manifest . get_entry ( & matches . value_of ( " username " ) . unwrap ( ) . into ( ) )
} else {
2022-02-22 15:38:41 +01:00
manifest
. entries
. first ( )
. 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 ) ;
}
return Ok ( selected_accounts ) ;
}
2021-08-14 17:24:15 +02:00
fn do_login ( account : & mut SteamGuardAccount ) -> anyhow ::Result < ( ) > {
2021-08-08 18:54:46 +02:00
if account . account_name . len ( ) > 0 {
println! ( " Username: {} " , account . account_name ) ;
} else {
print! ( " 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 ( ) ;
if password . len ( ) > 0 {
debug! ( " password is present " ) ;
} else {
debug! ( " password is empty " ) ;
}
2021-08-14 17:24:15 +02:00
account . session = Some ( do_login_impl (
account . account_name . clone ( ) ,
password ,
Some ( account ) ,
) ? ) ;
return Ok ( ( ) ) ;
2021-07-27 22:24:56 +02:00
}
2021-07-30 01:42:45 +02:00
2021-09-06 22:51:44 +02:00
fn do_login_raw ( username : String ) -> anyhow ::Result < steamapi ::Session > {
2021-08-09 00:32:50 +02:00
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 " ) ;
}
2021-08-14 17:24:15 +02:00
return do_login_impl ( username , password , None ) ;
}
fn do_login_impl (
username : String ,
password : String ,
account : Option < & SteamGuardAccount > ,
) -> anyhow ::Result < steamapi ::Session > {
2021-08-09 00:32:50 +02:00
// 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 ) ;
}
2021-08-14 17:24:15 +02:00
Err ( LoginError ::Need2FA ) = > match account {
Some ( a ) = > {
let server_time = steamapi ::get_server_time ( ) ;
login . twofactor_code = a . generate_code ( server_time ) ;
}
None = > {
print! ( " Enter 2fa code: " ) ;
login . twofactor_code = tui ::prompt ( ) ;
}
} ,
2021-08-09 00:32:50 +02:00
Err ( LoginError ::NeedCaptcha { captcha_gid } ) = > {
debug! ( " need captcha to log in " ) ;
2021-08-14 16:01:25 +02:00
login . captcha_text = tui ::prompt_captcha_text ( & captcha_gid ) ;
2021-08-09 00:32:50 +02:00
}
Err ( LoginError ::NeedEmail ) = > {
println! ( " You should have received an email with a code. " ) ;
print! ( " Enter code: " ) ;
2021-08-14 16:01:25 +02:00
login . email_code = tui ::prompt ( ) ;
2021-08-09 00:32:50 +02:00
}
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. " ) ;
}
}
}
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 ( ) ;
}
2022-06-13 03:46:39 +02:00
2022-06-19 16:38:59 +02:00
fn do_subcmd_setup ( args : ArgsSetup , manifest : & mut accountmanager ::Manifest ) -> anyhow ::Result < ( ) > {
println! ( " Log in to the account that you want to link to steamguard-cli " ) ;
print! ( " Username: " ) ;
let username = args . username . unwrap_or ( tui ::prompt ( ) ) ;
if args . username . is_some ( ) {
println! ( " {} " , username ) ;
}
let account_name = username . clone ( ) ;
if manifest . account_exists ( & username ) {
bail! (
" Account {} already exists in manifest, remove it first " ,
username
) ;
}
let session =
do_login_raw ( username ) . expect ( " Failed to log in. Account has not been linked. " ) ;
2022-06-13 03:46:39 +02:00
2022-06-19 16:38:59 +02:00
let mut linker = AccountLinker ::new ( session ) ;
let account : SteamGuardAccount ;
loop {
match linker . link ( ) {
Ok ( a ) = > {
account = a ;
break ;
}
Err ( AccountLinkError ::MustRemovePhoneNumber ) = > {
println! ( " There is already a phone number on this account, please remove it and try again. " ) ;
bail! ( " There is already a phone number on this account, please remove it and try again. " ) ;
}
Err ( AccountLinkError ::MustProvidePhoneNumber ) = > {
println! ( " Enter your phone number in the following format: +1 123-456-7890 " ) ;
print! ( " Phone number: " ) ;
linker . phone_number = tui ::prompt ( ) . replace ( & [ '(' , ')' , '-' ] [ .. ] , " " ) ;
}
Err ( AccountLinkError ::AuthenticatorPresent ) = > {
println! ( " An authenticator is already present on this account. " ) ;
bail! ( " An authenticator is already present on this account. " ) ;
}
Err ( AccountLinkError ::MustConfirmEmail ) = > {
println! ( " Check your email and click the link. " ) ;
tui ::pause ( ) ;
}
Err ( err ) = > {
error! (
" Failed to link authenticator. Account has not been linked. {} " ,
err
) ;
return Err ( err . into ( ) ) ;
}
}
}
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 . get_account ( & account_name ) . unwrap ( ) . lock ( ) . unwrap ( )
) ;
return Err ( err . into ( ) ) ;
}
}
let account_arc = manifest . get_account ( & account_name ) . unwrap ( ) ;
let mut account = account_arc . lock ( ) . unwrap ( ) ;
println! ( " Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {} " , account . revocation_code ) ;
tui ::pause ( ) ;
debug! ( " attempting link finalization " ) ;
print! ( " Enter SMS code: " ) ;
let sms_code = tui ::prompt ( ) ;
let mut tries = 0 ;
loop {
match linker . finalize ( & mut account , sms_code . clone ( ) ) {
Ok ( _ ) = > break ,
Err ( FinalizeLinkError ::WantMore ) = > {
debug! ( " steam wants more 2fa codes (tries: {}) " , tries ) ;
tries + = 1 ;
if tries > = 30 {
error! ( " Failed to finalize: unable to generate valid 2fa codes " ) ;
bail! ( " Failed to finalize: unable to generate valid 2fa codes " ) ;
}
}
Err ( err ) = > {
error! ( " Failed to finalize: {} " , err ) ;
return Err ( err . into ( ) ) ;
}
}
}
println! ( " Authenticator finalized. " ) ;
match manifest . save ( ) {
Ok ( _ ) = > { }
Err ( err ) = > {
println! (
" Failed to save manifest, but we were able to save it before. {} " ,
err
) ;
return Err ( err ) ;
}
}
println! (
" Authenticator has been finalized. Please actually write down your revocation code: {} " ,
account . revocation_code
) ;
return Ok ( ( ) ) ;
2022-06-13 03:46:39 +02:00
}