diff --git a/Cargo.lock b/Cargo.lock index f094763..c283ace 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2096,6 +2096,7 @@ dependencies = [ "text_io", "thiserror", "uuid", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 84f1398..57c615d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ crossterm = { version = "0.23.2", features = ["event-stream"] } qrcode = { version = "0.12.0", optional = true } gethostname = "0.4.3" secrecy = { version = "0.8", features = ["serde"] } +zeroize = "^1.4.3" [dev-dependencies] tempdir = "0.3" diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 9ed18a8..fc6891d 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -7,7 +7,7 @@ use std::fs::File; use std::io::{BufReader, Read, Write}; use std::path::Path; use std::sync::{Arc, Mutex}; -use steamguard::{ExposeSecret, SteamGuardAccount}; +use steamguard::SteamGuardAccount; use thiserror::Error; mod legacy; diff --git a/src/accountmanager/legacy.rs b/src/accountmanager/legacy.rs index 7e7d8d7..3b8e637 100644 --- a/src/accountmanager/legacy.rs +++ b/src/accountmanager/legacy.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use std::{ fs::File, io::{BufReader, Read}, @@ -5,9 +7,10 @@ use std::{ }; use log::debug; -use secrecy::ExposeSecret; +use secrecy::{CloneableSecret, DebugSecret, ExposeSecret}; use serde::Deserialize; use steamguard::{token::TwoFactorSecret, SecretString, SteamGuardAccount}; +use zeroize::Zeroize; use crate::encryption::{EncryptionScheme, EntryEncryptor}; @@ -75,8 +78,7 @@ impl EntryLoader for SdaManifestEntry { debug!("loading entry: {:?}", path); let file = File::open(path)?; let mut reader = BufReader::new(file); - let account: SdaAccount; - match (&passkey, encryption_params.as_ref()) { + let account: SdaAccount = match (&passkey, encryption_params.as_ref()) { (Some(passkey), Some(params)) => { let mut ciphertext: Vec = vec![]; reader.read_to_end(&mut ciphertext)?; @@ -86,14 +88,12 @@ impl EntryLoader for SdaManifestEntry { return Err(ManifestAccountLoadError::IncorrectPasskey); } let s = std::str::from_utf8(&plaintext).unwrap(); - account = serde_json::from_str(s)?; + serde_json::from_str(s)? } (None, Some(_)) => { return Err(ManifestAccountLoadError::MissingPasskey); } - (_, None) => { - account = serde_json::from_reader(reader)?; - } + (_, None) => serde_json::from_reader(reader)?, }; Ok(account) } @@ -135,9 +135,30 @@ pub struct SdaAccount { #[serde(with = "crate::secret_string")] pub secret_1: SecretString, #[serde(default, rename = "Session")] - pub session: Option>, + pub session: Option>, } +#[derive(Debug, Clone, Deserialize, Zeroize)] +#[zeroize(drop)] +#[deprecated(note = "this is not used anymore, the closest equivalent is `Tokens`")] +pub struct Session { + #[serde(rename = "SessionID")] + pub session_id: String, + #[serde(rename = "SteamLogin")] + pub steam_login: String, + #[serde(rename = "SteamLoginSecure")] + pub steam_login_secure: String, + #[serde(default, rename = "WebCookie")] + pub web_cookie: Option, + #[serde(rename = "OAuthToken")] + pub token: String, + #[serde(rename = "SteamID")] + pub steam_id: u64, +} + +impl CloneableSecret for Session {} +impl DebugSecret for Session {} + impl From for SteamGuardAccount { fn from(value: SdaAccount) -> Self { let steam_id = value diff --git a/src/accountmanager/migrate.rs b/src/accountmanager/migrate.rs index 9ff5759..9141cad 100644 --- a/src/accountmanager/migrate.rs +++ b/src/accountmanager/migrate.rs @@ -40,8 +40,8 @@ fn do_migrate( let mut file = File::open(manifest_path)?; let mut buffer = String::new(); file.read_to_string(&mut buffer)?; - let mut manifest: MigratingManifest = deserialize_manifest(buffer) - .map_err(|err| MigrationError::ManifestDeserializeFailed(err))?; + let mut manifest: MigratingManifest = + deserialize_manifest(buffer).map_err(MigrationError::ManifestDeserializeFailed)?; if manifest.is_encrypted() && passkey.is_none() { return Err(MigrationError::MissingPasskey); @@ -94,35 +94,25 @@ pub(crate) enum MigrationError { #[derive(Debug)] enum MigratingManifest { - SDA(SdaManifest), + Sda(SdaManifest), ManifestV1(ManifestV1), } impl MigratingManifest { pub fn upgrade(self) -> Self { match self { - Self::SDA(sda) => Self::ManifestV1(sda.into()), + Self::Sda(sda) => Self::ManifestV1(sda.into()), Self::ManifestV1(_) => self, } } pub fn is_latest(&self) -> bool { - match self { - Self::ManifestV1(_) => true, - _ => false, - } - } - - pub fn version(&self) -> u32 { - match self { - Self::SDA(_) => 0, - Self::ManifestV1(_) => 1, - } + matches!(self, Self::ManifestV1(_)) } pub fn is_encrypted(&self) -> bool { match self { - Self::SDA(manifest) => manifest.entries.iter().any(|e| e.encryption.is_some()), + Self::Sda(manifest) => manifest.entries.iter().any(|e| e.encryption.is_some()), Self::ManifestV1(manifest) => manifest.entries.iter().any(|e| e.encryption.is_some()), } } @@ -134,7 +124,7 @@ impl MigratingManifest { ) -> anyhow::Result> { debug!("loading all accounts for migration"); let accounts = match self { - Self::SDA(sda) => { + Self::Sda(sda) => { let (accounts, errors) = sda .entries .iter() @@ -201,7 +191,7 @@ fn deserialize_manifest(text: String) -> Result bool { - match self { - Self::ManifestV1(_) => true, - _ => false, - } + matches!(self, Self::ManifestV1(_)) } } diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 4264650..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,193 +0,0 @@ -use clap::{clap_derive::ArgEnum, Parser}; -use clap_complete::Shell; -use std::str::FromStr; - -#[derive(Debug, Clone, Parser)] -#[clap(name="steamguard-cli", bin_name="steamguard", author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] -pub(crate) struct Args { - #[clap( - short, - long, - conflicts_with = "all", - 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." - )] - pub username: Option, - #[clap( - short, - long, - conflicts_with = "username", - help = "Select all accounts in the manifest." - )] - pub all: bool, - /// The path to the maFiles directory. - #[clap( - short, - long, - help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. Default: ~/.config/steamguard-cli/maFiles" - )] - pub mafiles_path: Option, - #[clap( - short, - long, - env = "STEAMGUARD_CLI_PASSKEY", - help = "Specify your encryption passkey." - )] - pub passkey: Option, - #[clap(short, long, arg_enum, default_value_t=Verbosity::Info, help = "Set the log level. Be warned, trace is capable of printing sensitive data.")] - pub verbosity: Verbosity, - - #[clap(subcommand)] - pub sub: Option, - - #[clap(flatten)] - pub code: ArgsCode, -} - -#[derive(Debug, Clone, Parser)] -pub(crate) enum Subcommands { - Debug(ArgsDebug), - Completion(ArgsCompletions), - Setup(ArgsSetup), - Import(ArgsImport), - Trade(ArgsTrade), - Remove(ArgsRemove), - Encrypt(ArgsEncrypt), - Decrypt(ArgsDecrypt), - Code(ArgsCode), - #[cfg(feature = "qr")] - Qr(ArgsQr), - #[cfg(debug_assertions)] - TestLogin, -} - -#[derive(Debug, Clone, Copy, ArgEnum)] -pub(crate) 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 { - 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)), - } - } -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Debug stuff, not useful for most users.")] -pub(crate) struct ArgsDebug { - #[clap(long, help = "Show a text prompt.")] - pub demo_prompt: bool, - #[clap(long, help = "Show a \"press any key\" prompt.")] - pub demo_pause: bool, - #[clap(long, help = "Show a character prompt.")] - pub demo_prompt_char: bool, - #[clap(long, help = "Show an example confirmation menu using dummy data.")] - pub demo_conf_menu: bool, -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Generate shell completions")] -pub(crate) struct ArgsCompletions { - #[clap(short, long, arg_enum, help = "The shell to generate completions for.")] - pub shell: Shell, -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Set up a new account with steamguard-cli")] -pub(crate) struct ArgsSetup {} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Import an account with steamguard already set up")] -pub(crate) struct ArgsImport { - #[clap(long, help = "Whether or not the provided maFiles are from SDA.")] - pub sda: bool, - - #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] - pub files: Vec, -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Interactive interface for trade confirmations")] -pub(crate) struct ArgsTrade { - #[clap( - short, - long, - help = "Accept all open trade confirmations. Does not open interactive interface." - )] - pub accept_all: bool, - #[clap( - short, - long, - help = "If submitting a confirmation response fails, exit immediately." - )] - pub fail_fast: bool, -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Remove the authenticator from an account.")] -pub(crate) struct ArgsRemove; - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Encrypt all maFiles")] -pub(crate) struct ArgsEncrypt; - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Decrypt all maFiles")] -pub(crate) struct ArgsDecrypt; - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Generate 2FA codes")] -pub(crate) struct ArgsCode { - #[clap( - long, - help = "Assume the computer's time is correct. Don't ask Steam for the time when generating codes." - )] - pub offline: bool, -} - -// HACK: the derive API doesn't support default subcommands, so we are going to make it so that it'll be easier to switch over when it's implemented. -// See: https://github.com/clap-rs/clap/issues/3857 -impl From for ArgsCode { - fn from(args: Args) -> Self { - args.code - } -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Generate QR codes. This *will* print sensitive data to stdout.")] -#[cfg(feature = "qr")] -pub(crate) struct ArgsQr { - #[clap( - long, - help = "Force using ASCII chars to generate QR codes. Useful for terminals that don't support unicode." - )] - pub ascii: bool, -} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..aa25e49 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,167 @@ +use std::sync::{Arc, Mutex}; + +use clap::{clap_derive::ArgEnum, Parser}; +use clap_complete::Shell; +use std::str::FromStr; +use steamguard::SteamGuardAccount; + +use crate::AccountManager; + +pub mod code; +pub mod completions; +pub mod debug; +pub mod decrypt; +pub mod encrypt; +pub mod import; +#[cfg(feature = "qr")] +pub mod qr; +pub mod remove; +pub mod setup; +pub mod trade; + +pub use code::CodeCommand; +pub use completions::CompletionsCommand; +pub use debug::DebugCommand; +pub use decrypt::DecryptCommand; +pub use encrypt::EncryptCommand; +pub use import::ImportCommand; +#[cfg(feature = "qr")] +pub use qr::QrCommand; +pub use remove::RemoveCommand; +pub use setup::SetupCommand; +pub use trade::TradeCommand; + +/// A command that does not operate on the manifest or individual accounts. +pub(crate) trait ConstCommand { + fn execute(&self) -> anyhow::Result<()>; +} + +/// A command that operates the manifest as a whole +pub(crate) trait ManifestCommand { + fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()>; +} + +/// A command that operates on individual accounts. +pub(crate) trait AccountCommand { + fn execute( + &self, + manager: &mut AccountManager, + accounts: Vec>>, + ) -> anyhow::Result<()>; +} + +pub(crate) enum CommandType { + Const(Box), + Manifest(Box), + Account(Box), +} + +#[derive(Debug, Clone, Parser)] +#[clap(name="steamguard-cli", bin_name="steamguard", author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] +pub(crate) struct Args { + #[clap(flatten)] + pub global: GlobalArgs, + + #[clap(subcommand)] + pub sub: Option, + + #[clap(flatten)] + pub code: CodeCommand, +} + +#[derive(Debug, Clone, Parser)] +pub(crate) struct GlobalArgs { + #[clap( + short, + long, + conflicts_with = "all", + 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." + )] + pub username: Option, + #[clap( + short, + long, + conflicts_with = "username", + help = "Select all accounts in the manifest." + )] + pub all: bool, + /// The path to the maFiles directory. + #[clap( + short, + long, + help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. Default: ~/.config/steamguard-cli/maFiles" + )] + pub mafiles_path: Option, + #[clap( + short, + long, + env = "STEAMGUARD_CLI_PASSKEY", + help = "Specify your encryption passkey." + )] + pub passkey: Option, + #[clap(short, long, arg_enum, default_value_t=Verbosity::Info, help = "Set the log level. Be warned, trace is capable of printing sensitive data.")] + pub verbosity: Verbosity, +} + +#[derive(Debug, Clone, Parser)] +pub(crate) enum Subcommands { + Debug(DebugCommand), + Completion(CompletionsCommand), + Setup(SetupCommand), + Import(ImportCommand), + Trade(TradeCommand), + Remove(RemoveCommand), + Encrypt(EncryptCommand), + Decrypt(DecryptCommand), + Code(CodeCommand), + #[cfg(feature = "qr")] + Qr(QrCommand), +} + +#[derive(Debug, Clone, Copy, ArgEnum)] +pub(crate) 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 { + 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)), + } + } +} + +// HACK: the derive API doesn't support default subcommands, so we are going to make it so that it'll be easier to switch over when it's implemented. +// See: https://github.com/clap-rs/clap/issues/3857 +impl From for CodeCommand { + fn from(args: Args) -> Self { + args.code + } +} diff --git a/src/commands/code.rs b/src/commands/code.rs new file mode 100644 index 0000000..1a9e84c --- /dev/null +++ b/src/commands/code.rs @@ -0,0 +1,45 @@ +use std::{ + sync::{Arc, Mutex}, + time::{SystemTime, UNIX_EPOCH}, +}; + +use log::*; +use steamguard::{steamapi, SteamGuardAccount}; + +use crate::AccountManager; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Generate 2FA codes")] +pub struct CodeCommand { + #[clap( + long, + help = "Assume the computer's time is correct. Don't ask Steam for the time when generating codes." + )] + pub offline: bool, +} + +impl AccountCommand for CodeCommand { + fn execute( + &self, + _manager: &mut AccountManager, + accounts: Vec>>, + ) -> anyhow::Result<()> { + let server_time = if self.offline { + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + } else { + steamapi::get_server_time()?.server_time() + }; + debug!("Time used to generate codes: {}", server_time); + + for account in accounts { + let account = account.lock().unwrap(); + info!("Generating code for {}", account.account_name); + trace!("{:?}", account); + let code = account.generate_code(server_time); + println!("{}", code); + } + Ok(()) + } +} diff --git a/src/commands/completions.rs b/src/commands/completions.rs new file mode 100644 index 0000000..fc09934 --- /dev/null +++ b/src/commands/completions.rs @@ -0,0 +1,18 @@ +use clap::CommandFactory; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Generate shell completions")] +pub struct CompletionsCommand { + #[clap(short, long, arg_enum, help = "The shell to generate completions for.")] + pub shell: Shell, +} + +impl ConstCommand for CompletionsCommand { + fn execute(&self) -> anyhow::Result<()> { + let mut app = Args::command_for_update(); + clap_complete::generate(self.shell, &mut app, "steamguard", &mut std::io::stdout()); + Ok(()) + } +} diff --git a/src/demos.rs b/src/commands/debug.rs similarity index 67% rename from src/demos.rs rename to src/commands/debug.rs index 794ca2e..70b608c 100644 --- a/src/demos.rs +++ b/src/commands/debug.rs @@ -1,7 +1,41 @@ -use crate::tui; use log::*; use steamguard::{Confirmation, ConfirmationType}; +use crate::tui; + +use super::*; + +#[derive(Debug, Clone, Parser, Default)] +#[clap(about = "Debug stuff, not useful for most users.")] +pub struct DebugCommand { + #[clap(long, help = "Show a text prompt.")] + pub demo_prompt: bool, + #[clap(long, help = "Show a \"press any key\" prompt.")] + pub demo_pause: bool, + #[clap(long, help = "Show a character prompt.")] + pub demo_prompt_char: bool, + #[clap(long, help = "Show an example confirmation menu using dummy data.")] + pub demo_conf_menu: bool, +} + +impl ConstCommand for DebugCommand { + fn execute(&self) -> anyhow::Result<()> { + if self.demo_prompt { + demo_prompt(); + } + if self.demo_pause { + demo_pause(); + } + if self.demo_prompt_char { + demo_prompt_char(); + } + if self.demo_conf_menu { + demo_confirmation_menu(); + } + Ok(()) + } +} + pub fn demo_prompt() { print!("Prompt: "); let result = tui::prompt(); diff --git a/src/commands/decrypt.rs b/src/commands/decrypt.rs new file mode 100644 index 0000000..7f847fb --- /dev/null +++ b/src/commands/decrypt.rs @@ -0,0 +1,43 @@ +use log::*; + +use crate::{AccountManager, ManifestAccountLoadError}; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Decrypt all maFiles")] +pub struct DecryptCommand; + +impl ManifestCommand for DecryptCommand { + fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { + load_accounts_with_prompts(manager)?; + for mut entry in manager.iter_mut() { + entry.encryption = None; + } + manager.submit_passkey(None); + manager.save()?; + Ok(()) + } +} + +fn load_accounts_with_prompts(manager: &mut AccountManager) -> anyhow::Result<()> { + loop { + match manager.load_accounts() { + Ok(_) => return Ok(()), + Err( + ManifestAccountLoadError::MissingPasskey + | ManifestAccountLoadError::IncorrectPasskey, + ) => { + if manager.has_passkey() { + error!("Incorrect passkey"); + } + let passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); + manager.submit_passkey(passkey); + } + Err(e) => { + error!("Could not load accounts: {}", e); + return Err(e.into()); + } + } + } +} diff --git a/src/commands/encrypt.rs b/src/commands/encrypt.rs new file mode 100644 index 0000000..b475db6 --- /dev/null +++ b/src/commands/encrypt.rs @@ -0,0 +1,39 @@ +use log::*; + +use crate::AccountManager; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Encrypt all maFiles")] +pub struct EncryptCommand; + +impl ManifestCommand for EncryptCommand { + fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { + if !manager.has_passkey() { + let mut passkey; + loop { + passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); + if let Some(p) = passkey.as_ref() { + if p.is_empty() { + error!("Passkey cannot be empty, try again."); + continue; + } + } + let passkey_confirm = + rpassword::prompt_password_stdout("Confirm encryption passkey: ").ok(); + if passkey == passkey_confirm { + break; + } + error!("Passkeys do not match, try again."); + } + manager.submit_passkey(passkey); + } + manager.load_accounts()?; + for entry in manager.iter_mut() { + entry.encryption = Some(crate::accountmanager::EntryEncryptionParams::generate()); + } + manager.save()?; + Ok(()) + } +} diff --git a/src/commands/import.rs b/src/commands/import.rs new file mode 100644 index 0000000..5ec990b --- /dev/null +++ b/src/commands/import.rs @@ -0,0 +1,42 @@ +use std::path::Path; + +use log::*; + +use crate::AccountManager; + +use super::*; + +#[derive(Debug, Clone, Parser, Default)] +#[clap(about = "Import an account with steamguard already set up")] +pub struct ImportCommand { + #[clap(long, help = "Whether or not the provided maFiles are from SDA.")] + pub sda: bool, + + #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] + pub files: Vec, +} + +impl ManifestCommand for ImportCommand { + fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { + for file_path in self.files.iter() { + if self.sda { + let path = Path::new(&file_path); + let account = crate::accountmanager::migrate::load_and_upgrade_sda_account(path)?; + manager.add_account(account); + info!("Imported account: {}", &file_path); + } else { + match manager.import_account(file_path) { + Ok(_) => { + info!("Imported account: {}", &file_path); + } + Err(err) => { + bail!("Failed to import account: {} {}", &file_path, err); + } + } + } + } + + manager.save()?; + Ok(()) + } +} diff --git a/src/commands/qr.rs b/src/commands/qr.rs new file mode 100644 index 0000000..6acda6e --- /dev/null +++ b/src/commands/qr.rs @@ -0,0 +1,55 @@ +use std::sync::{Arc, Mutex}; + +use log::*; +use qrcode::QrCode; +use secrecy::ExposeSecret; + +use crate::AccountManager; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Generate QR codes. This *will* print sensitive data to stdout.")] +pub struct QrCommand { + #[clap( + long, + help = "Force using ASCII chars to generate QR codes. Useful for terminals that don't support unicode." + )] + pub ascii: bool, +} + +impl AccountCommand for QrCommand { + fn execute( + &self, + _manager: &mut AccountManager, + accounts: Vec>>, + ) -> anyhow::Result<()> { + use anyhow::Context; + + info!("Generating QR codes for {} accounts", accounts.len()); + + for account in accounts { + let account = account.lock().unwrap(); + let qr = QrCode::new(account.uri.expose_secret()) + .context(format!("generating qr code for {}", account.account_name))?; + + info!("Printing QR code for {}", account.account_name); + let qr_string = if self.ascii { + qr.render() + .light_color(' ') + .dark_color('#') + .module_dimensions(2, 1) + .build() + } else { + use qrcode::render::unicode; + qr.render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build() + }; + + println!("{}", qr_string); + } + Ok(()) + } +} diff --git a/src/commands/remove.rs b/src/commands/remove.rs new file mode 100644 index 0000000..bcc743f --- /dev/null +++ b/src/commands/remove.rs @@ -0,0 +1,80 @@ +use std::sync::{Arc, Mutex}; + +use log::*; + +use crate::{errors::UserError, tui, AccountManager}; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Remove the authenticator from an account.")] +pub struct RemoveCommand; + +impl AccountCommand for RemoveCommand { + fn execute( + &self, + manager: &mut AccountManager, + accounts: Vec>>, + ) -> anyhow::Result<()> { + eprintln!( + "This will remove the mobile authenticator from {} accounts: {}", + accounts.len(), + accounts + .iter() + .map(|a| a.lock().unwrap().account_name.clone()) + .collect::>() + .join(", ") + ); + + match tui::prompt_char("Do you want to continue?", "yN") { + 'y' => {} + _ => { + info!("Aborting!"); + return Err(UserError::Aborted.into()); + } + } + + let mut successful = vec![]; + for a in accounts { + let mut account = a.lock().unwrap(); + if !account.is_logged_in() { + info!("Account does not have tokens, logging in"); + crate::do_login(&mut account)?; + } + + match account.remove_authenticator(None) { + Ok(success) => { + if success { + println!("Removed authenticator from {}", account.account_name); + successful.push(account.account_name.clone()); + } else { + println!( + "Failed to remove authenticator from {}", + account.account_name + ); + if tui::prompt_char( + "Would you like to remove it from the manifest anyway?", + "yN", + ) == 'y' + { + successful.push(account.account_name.clone()); + } + } + } + Err(err) => { + error!( + "Unexpected error when removing authenticator from {}: {}", + account.account_name, err + ); + } + } + } + + for account_name in successful { + manager.remove_account(account_name); + } + + manager.save()?; + Ok(()) + } +} diff --git a/src/commands/setup.rs b/src/commands/setup.rs new file mode 100644 index 0000000..7a72483 --- /dev/null +++ b/src/commands/setup.rs @@ -0,0 +1,137 @@ +use log::*; +use secrecy::ExposeSecret; +use steamguard::{ + accountlinker::AccountLinkSuccess, AccountLinkError, AccountLinker, FinalizeLinkError, +}; + +use crate::{tui, AccountManager}; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Set up a new account with steamguard-cli")] +pub struct SetupCommand; + +impl ManifestCommand for SetupCommand { + fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { + eprintln!("Log in to the account that you want to link to steamguard-cli"); + eprint!("Username: "); + let username = tui::prompt().to_lowercase(); + let account_name = username.clone(); + if manager.account_exists(&username) { + bail!( + "Account {} already exists in manifest, remove it first", + username + ); + } + info!("Logging in to {}", username); + let session = + crate::do_login_raw(username).expect("Failed to log in. Account has not been linked."); + + info!("Adding authenticator..."); + let mut linker = AccountLinker::new(session); + let link: AccountLinkSuccess; + loop { + match linker.link() { + Ok(a) => { + link = 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()); + } + } + } + let mut server_time = link.server_time(); + let phone_number_hint = link.phone_number_hint().to_owned(); + manager.add_account(link.into_account()); + match manager.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{:#?}", + manager.get_account(&account_name).unwrap().lock().unwrap() + ); + return Err(err); + } + } + + let account_arc = manager + .get_account(&account_name) + .expect("account was not present in manifest"); + 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.expose_secret()); + tui::pause(); + + debug!("attempting link finalization"); + println!( + "A code has been sent to your phone number ending in {}.", + phone_number_hint + ); + print!("Enter SMS code: "); + let sms_code = tui::prompt(); + let mut tries = 0; + loop { + match linker.finalize(server_time, &mut account, sms_code.clone()) { + Ok(_) => break, + Err(FinalizeLinkError::WantMore { server_time: s }) => { + server_time = s; + 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()); + } + } + } + let revocation_code = account.revocation_code.clone(); + drop(account); // explicitly drop the lock so we don't hang on the mutex + + println!("Authenticator finalized."); + match manager.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: {}", + revocation_code.expose_secret() + ); + + Ok(()) + } +} diff --git a/src/commands/trade.rs b/src/commands/trade.rs new file mode 100644 index 0000000..4315520 --- /dev/null +++ b/src/commands/trade.rs @@ -0,0 +1,115 @@ +use std::sync::{Arc, Mutex}; + +use crossterm::tty::IsTty; +use log::*; +use steamguard::Confirmation; + +use crate::{tui, AccountManager}; + +use super::*; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Interactive interface for trade confirmations")] +pub struct TradeCommand { + #[clap( + short, + long, + help = "Accept all open trade confirmations. Does not open interactive interface." + )] + pub accept_all: bool, + #[clap( + short, + long, + help = "If submitting a confirmation response fails, exit immediately." + )] + pub fail_fast: bool, +} + +impl AccountCommand for TradeCommand { + fn execute( + &self, + manager: &mut AccountManager, + accounts: Vec>>, + ) -> anyhow::Result<()> { + for a in accounts { + let mut account = a.lock().unwrap(); + + if !account.is_logged_in() { + info!("Account does not have tokens, logging in"); + crate::do_login(&mut account)?; + } + + info!("Checking for trade confirmations"); + let confirmations: Vec; + loop { + match account.get_trade_confirmations() { + Ok(confs) => { + confirmations = confs; + break; + } + Err(err) => { + error!("Failed to get trade confirmations: {:#?}", err); + info!("failed to get trade confirmations, asking user to log in"); + crate::do_login(&mut account)?; + } + } + } + + let mut any_failed = false; + if self.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 self.fail_fast { + return result; + } + } else { + debug!("accept confirmation result: {:?}", result); + } + } + } else if std::io::stdout().is_tty() { + 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 self.fail_fast { + return result; + } + } else { + debug!("accept confirmation result: {:?}", result); + } + } + 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 self.fail_fast { + return result; + } + } else { + debug!("deny confirmation result: {:?}", result); + } + } + } else { + warn!("not a tty, not showing menu"); + for conf in &confirmations { + println!("{}", conf.description()); + } + } + + if any_failed { + error!("Failed to respond to some confirmations."); + } + } + + manager.save()?; + Ok(()) + } +} diff --git a/src/encryption.rs b/src/encryption.rs index 3b7ba2f..a28b650 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -45,12 +45,12 @@ impl Default for EncryptionScheme { pub trait EntryEncryptor { fn encrypt( - passkey: &String, + passkey: &str, params: &EntryEncryptionParams, plaintext: Vec, ) -> anyhow::Result, EntryEncryptionError>; fn decrypt( - passkey: &String, + passkey: &str, params: &EntryEncryptionParams, ciphertext: Vec, ) -> anyhow::Result, EntryEncryptionError>; @@ -63,10 +63,7 @@ impl LegacySdaCompatible { const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive, but necessary to maintain compatibility with SteamDesktopAuthenticator. const KEY_SIZE_BYTES: usize = 32; - fn get_encryption_key( - passkey: &String, - salt: &String, - ) -> anyhow::Result<[u8; Self::KEY_SIZE_BYTES]> { + fn get_encryption_key(passkey: &str, salt: &str) -> anyhow::Result<[u8; Self::KEY_SIZE_BYTES]> { let password_bytes = passkey.as_bytes(); let salt_bytes = base64::decode(salt)?; let mut full_key: [u8; Self::KEY_SIZE_BYTES] = [0u8; Self::KEY_SIZE_BYTES]; @@ -86,11 +83,11 @@ impl EntryEncryptor for LegacySdaCompatible { // ngl, this logic sucks ass. its kinda annoying that the logic is not completely symetric. fn encrypt( - passkey: &String, + passkey: &str, params: &EntryEncryptionParams, plaintext: Vec, ) -> anyhow::Result, EntryEncryptionError> { - let key = Self::get_encryption_key(&passkey.into(), ¶ms.salt)?; + let key = Self::get_encryption_key(passkey, ¶ms.salt)?; let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; @@ -116,11 +113,11 @@ impl EntryEncryptor for LegacySdaCompatible { } fn decrypt( - passkey: &String, + passkey: &str, params: &EntryEncryptionParams, ciphertext: Vec, ) -> anyhow::Result, EntryEncryptionError> { - let key = Self::get_encryption_key(&passkey.into(), ¶ms.salt)?; + let key = Self::get_encryption_key(passkey, ¶ms.salt)?; let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; @@ -181,16 +178,14 @@ mod tests { #[test] fn test_encryption_key() { assert_eq!( - LegacySdaCompatible::get_encryption_key(&"password".into(), &"GMhL0N2hqXg=".into()) - .unwrap(), + LegacySdaCompatible::get_encryption_key("password", "GMhL0N2hqXg=").unwrap(), base64::decode("KtiRa4/OxW83MlB6URf+Z8rAGj7CBY+pDlwD/NuVo6Y=") .unwrap() .as_slice() ); assert_eq!( - LegacySdaCompatible::get_encryption_key(&"password".into(), &"wTzTE9A6aN8=".into()) - .unwrap(), + LegacySdaCompatible::get_encryption_key("password", "wTzTE9A6aN8=").unwrap(), base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=") .unwrap() .as_slice() @@ -202,9 +197,8 @@ mod tests { let passkey = "password"; let params = EntryEncryptionParams::generate(); let orig = "tactical glizzy".as_bytes().to_vec(); - let encrypted = - LegacySdaCompatible::encrypt(&passkey.clone().into(), ¶ms, orig.clone()).unwrap(); - let result = LegacySdaCompatible::decrypt(&passkey.into(), ¶ms, encrypted).unwrap(); + let encrypted = LegacySdaCompatible::encrypt(passkey, ¶ms, orig.clone()).unwrap(); + let result = LegacySdaCompatible::decrypt(passkey, ¶ms, encrypted).unwrap(); assert_eq!(orig, result.to_vec()); Ok(()) } diff --git a/src/errors.rs b/src/errors.rs index b61bdb0..0def6f0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,6 +4,4 @@ use thiserror::Error; pub(crate) enum UserError { #[error("User aborted the operation.")] Aborted, - #[error("Unknown subcommand. It may need to be implemented.")] - UnknownSubcommand, } diff --git a/src/main.rs b/src/main.rs index d8d85a2..43fc26e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,21 @@ extern crate rpassword; -use clap::{IntoApp, Parser}; -use crossterm::tty::IsTty; +use clap::Parser; use log::*; -#[cfg(feature = "qr")] -use qrcode::QrCode; -use std::time::{SystemTime, UNIX_EPOCH}; use std::{ - io::{stdout, Write}, + io::Write, path::Path, sync::{Arc, Mutex}, }; -use steamguard::accountlinker::AccountLinkSuccess; use steamguard::protobufs::steammessages_auth_steamclient::{ EAuthSessionGuardType, EAuthTokenPlatformType, }; use steamguard::token::Tokens; -use steamguard::{ - steamapi, AccountLinkError, AccountLinker, Confirmation, DeviceDetails, ExposeSecret, - FinalizeLinkError, LoginError, SteamGuardAccount, UserLogin, -}; +use steamguard::{steamapi, DeviceDetails, LoginError, SteamGuardAccount, UserLogin}; use crate::accountmanager::migrate::load_and_migrate; -use crate::accountmanager::{AccountManager, ManifestAccountLoadError, ManifestLoadError}; +pub use crate::accountmanager::{AccountManager, ManifestAccountLoadError, ManifestLoadError}; +use crate::commands::{CommandType, Subcommands}; -#[macro_use] extern crate lazy_static; #[macro_use] extern crate anyhow; @@ -33,12 +25,10 @@ extern crate dirs; extern crate proptest; extern crate ring; mod accountmanager; -mod cli; -mod demos; +mod commands; mod encryption; mod errors; mod secret_string; -mod test_login; pub(crate) mod tui; fn main() { @@ -52,27 +42,37 @@ fn main() { } fn run() -> anyhow::Result<()> { - let args = cli::Args::parse(); + let args = commands::Args::parse(); info!("{:?}", args); + let globalargs = args.global; + stderrlog::new() - .verbosity(args.verbosity as usize) + .verbosity(globalargs.verbosity as usize) .module(module_path!()) .module("steamguard") .init() .unwrap(); - match args.sub { - Some(cli::Subcommands::Debug(args)) => { - return do_subcmd_debug(args); - } - Some(cli::Subcommands::Completion(args)) => { - return do_subcmd_completion(args); - } - _ => {} + 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)), }; - let mafiles_dir = if let Some(mafiles_path) = &args.mafiles_path { + if let CommandType::Const(cmd) = cmd { + return cmd.execute(); + } + + let mafiles_dir = if let Some(mafiles_path) = &globalargs.mafiles_path { mafiles_path.clone() } else { get_mafiles_dir() @@ -82,15 +82,13 @@ fn run() -> anyhow::Result<()> { let mut manager: accountmanager::AccountManager; if !path.exists() { error!("Did not find manifest in {}", mafiles_dir); - match tui::prompt_char( + if tui::prompt_char( format!("Would you like to create a manifest in {} ?", mafiles_dir).as_str(), "Yn", - ) { - 'n' => { - info!("Aborting!"); - return Err(errors::UserError::Aborted.into()); - } - _ => {} + ) == 'n' + { + info!("Aborting!"); + return Err(errors::UserError::Aborted.into()); } std::fs::create_dir_all(mafiles_dir)?; @@ -101,10 +99,11 @@ fn run() -> anyhow::Result<()> { Ok(m) => m, Err(ManifestLoadError::MigrationNeeded) => { info!("Migrating manifest"); - let (manifest, accounts) = load_and_migrate(path.as_path(), args.passkey.as_ref())?; + let (manifest, accounts) = + load_and_migrate(path.as_path(), globalargs.passkey.as_ref())?; let mut manager = AccountManager::from_manifest(manifest, mafiles_dir); manager.register_accounts(accounts); - manager.submit_passkey(args.passkey.clone()); + manager.submit_passkey(globalargs.passkey.clone()); manager.save()?; manager } @@ -115,7 +114,7 @@ fn run() -> anyhow::Result<()> { } } - let mut passkey = args.passkey.clone(); + let mut passkey = globalargs.passkey.clone(); manager.submit_passkey(passkey); loop { @@ -146,25 +145,14 @@ fn run() -> anyhow::Result<()> { } } - match args.sub { - Some(cli::Subcommands::Setup(args)) => { - return do_subcmd_setup(args, &mut manager); - } - Some(cli::Subcommands::Import(args)) => { - return do_subcmd_import(args, &mut manager); - } - Some(cli::Subcommands::Encrypt(args)) => { - return do_subcmd_encrypt(args, &mut manager); - } - Some(cli::Subcommands::Decrypt(args)) => { - return do_subcmd_decrypt(args, &mut manager); - } - _ => {} + if let CommandType::Manifest(cmd) = cmd { + cmd.execute(&mut manager)?; + return Ok(()); } let selected_accounts: Vec>>; loop { - match get_selected_accounts(&args, &mut manager) { + match get_selected_accounts(&globalargs, &mut manager) { Ok(accounts) => { selected_accounts = accounts; break; @@ -194,23 +182,15 @@ fn run() -> anyhow::Result<()> { .collect::>() ); - match args.sub.unwrap_or(cli::Subcommands::Code(args.code)) { - cli::Subcommands::Trade(args) => do_subcmd_trade(args, &mut manager, selected_accounts), - cli::Subcommands::Remove(args) => do_subcmd_remove(args, &mut manager, selected_accounts), - cli::Subcommands::Code(args) => do_subcmd_code(args, selected_accounts), - #[cfg(feature = "qr")] - cli::Subcommands::Qr(args) => do_subcmd_qr(args, selected_accounts), - #[cfg(debug_assertions)] - cli::Subcommands::TestLogin => test_login::do_subcmd_test_login(selected_accounts), - s => { - error!("Unknown subcommand: {:?}", s); - Err(errors::UserError::UnknownSubcommand.into()) - } + if let CommandType::Account(cmd) = cmd { + return cmd.execute(&mut manager, selected_accounts); } + + Ok(()) } fn get_selected_accounts( - args: &cli::Args, + args: &commands::GlobalArgs, manifest: &mut accountmanager::AccountManager, ) -> anyhow::Result>>, ManifestAccountLoadError> { let mut selected_accounts: Vec>> = vec![]; @@ -274,13 +254,10 @@ fn do_login_impl( password: String, account: Option<&SteamGuardAccount>, ) -> anyhow::Result { - let mut login = UserLogin::new( - EAuthTokenPlatformType::k_EAuthTokenPlatformType_MobileApp, - build_device_details(), - ); + let mut login = UserLogin::new(build_device_details()); let mut password = password; - let mut confirmation_methods; + let confirmation_methods; loop { match login.begin_auth_via_credentials(&username, &password) { Ok(methods) => { @@ -306,7 +283,7 @@ fn do_login_impl( } } - for (method) in confirmation_methods { + for method in confirmation_methods { match method.confirmation_type { EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceConfirmation => { eprintln!("Please confirm this login on your other device."); @@ -325,7 +302,7 @@ fn do_login_impl( EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode => { let code = if let Some(account) = account { debug!("Generating 2fa code..."); - let time = steamapi::get_server_time()?.server_time; + let time = steamapi::get_server_time()?.server_time(); account.generate_code(time) } else { eprint!("Enter the 2fa code from your device: "); @@ -381,456 +358,3 @@ fn get_mafiles_dir() -> String { return paths[0].to_str().unwrap().into(); } - -fn load_accounts_with_prompts(manifest: &mut accountmanager::AccountManager) -> anyhow::Result<()> { - loop { - match manifest.load_accounts() { - Ok(_) => return Ok(()), - Err( - accountmanager::ManifestAccountLoadError::MissingPasskey - | accountmanager::ManifestAccountLoadError::IncorrectPasskey, - ) => { - if manifest.has_passkey() { - error!("Incorrect passkey"); - } - let 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()); - } - } - } -} - -fn do_subcmd_debug(args: cli::ArgsDebug) -> anyhow::Result<()> { - if args.demo_prompt { - demos::demo_prompt(); - } - if args.demo_pause { - demos::demo_pause(); - } - if args.demo_prompt_char { - demos::demo_prompt_char(); - } - if args.demo_conf_menu { - demos::demo_confirmation_menu(); - } - Ok(()) -} - -fn do_subcmd_completion(args: cli::ArgsCompletions) -> Result<(), anyhow::Error> { - let mut app = cli::Args::command_for_update(); - clap_complete::generate(args.shell, &mut app, "steamguard", &mut std::io::stdout()); - Ok(()) -} - -fn do_subcmd_setup( - _args: cli::ArgsSetup, - manifest: &mut accountmanager::AccountManager, -) -> anyhow::Result<()> { - eprintln!("Log in to the account that you want to link to steamguard-cli"); - eprint!("Username: "); - let username = tui::prompt().to_lowercase(); - let account_name = username.clone(); - if manifest.account_exists(&username) { - bail!( - "Account {} already exists in manifest, remove it first", - username - ); - } - info!("Logging in to {}", username); - let session = do_login_raw(username).expect("Failed to log in. Account has not been linked."); - - info!("Adding authenticator..."); - let mut linker = AccountLinker::new(session); - let link: AccountLinkSuccess; - loop { - match linker.link() { - Ok(a) => { - link = 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()); - } - } - } - let mut server_time = link.server_time(); - let phone_number_hint = link.phone_number_hint().to_owned(); - manifest.add_account(link.into_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); - } - } - - let account_arc = manifest - .get_account(&account_name) - .expect("account was not present in manifest"); - 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.expose_secret()); - tui::pause(); - - debug!("attempting link finalization"); - println!( - "A code has been sent to your phone number ending in {}.", - phone_number_hint - ); - print!("Enter SMS code: "); - let sms_code = tui::prompt(); - let mut tries = 0; - loop { - match linker.finalize(server_time, &mut account, sms_code.clone()) { - Ok(_) => break, - Err(FinalizeLinkError::WantMore { server_time: s }) => { - server_time = s; - 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()); - } - } - } - let revocation_code = account.revocation_code.clone(); - drop(account); // explicitly drop the lock so we don't hang on the mutex - - 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: {}", - revocation_code.expose_secret() - ); - - Ok(()) -} - -fn do_subcmd_import( - args: cli::ArgsImport, - manifest: &mut accountmanager::AccountManager, -) -> anyhow::Result<()> { - for file_path in args.files { - if args.sda { - let path = Path::new(&file_path); - let account = accountmanager::migrate::load_and_upgrade_sda_account(path)?; - manifest.add_account(account); - info!("Imported account: {}", &file_path); - } else { - match manifest.import_account(&file_path) { - Ok(_) => { - info!("Imported account: {}", &file_path); - } - Err(err) => { - bail!("Failed to import account: {} {}", &file_path, err); - } - } - } - } - - manifest.save()?; - Ok(()) -} - -fn do_subcmd_trade( - args: cli::ArgsTrade, - manifest: &mut accountmanager::AccountManager, - mut selected_accounts: Vec>>, -) -> anyhow::Result<()> { - for a in selected_accounts.iter_mut() { - let mut account = a.lock().unwrap(); - - if !account.is_logged_in() { - info!("Account does not have tokens, logging in"); - do_login(&mut account)?; - } - - info!("Checking for trade confirmations"); - let confirmations: Vec; - loop { - match account.get_trade_confirmations() { - Ok(confs) => { - confirmations = confs; - break; - } - Err(err) => { - error!("Failed to get trade confirmations: {:#?}", err); - info!("failed to get trade confirmations, asking user to log in"); - do_login(&mut account)?; - } - } - } - - let mut any_failed = false; - if args.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 args.fail_fast { - return result; - } - } else { - debug!("accept confirmation result: {:?}", result); - } - } - } else if stdout().is_tty() { - 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 args.fail_fast { - return result; - } - } else { - debug!("accept confirmation result: {:?}", result); - } - } - 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 args.fail_fast { - return result; - } - } else { - debug!("deny confirmation result: {:?}", result); - } - } - } else { - warn!("not a tty, not showing menu"); - for conf in &confirmations { - println!("{}", conf.description()); - } - } - - if any_failed { - error!("Failed to respond to some confirmations."); - } - } - - manifest.save()?; - Ok(()) -} - -fn do_subcmd_remove( - _args: cli::ArgsRemove, - manifest: &mut accountmanager::AccountManager, - selected_accounts: Vec>>, -) -> anyhow::Result<()> { - println!( - "This will remove the mobile authenticator from {} accounts: {}", - selected_accounts.len(), - selected_accounts - .iter() - .map(|a| a.lock().unwrap().account_name.clone()) - .collect::>() - .join(", ") - ); - - match tui::prompt_char("Do you want to continue?", "yN") { - 'y' => {} - _ => { - info!("Aborting!"); - return Err(errors::UserError::Aborted.into()); - } - } - - let mut successful = vec![]; - for a in selected_accounts { - let mut account = a.lock().unwrap(); - if !account.is_logged_in() { - info!("Account does not have tokens, logging in"); - do_login(&mut account)?; - } - - match account.remove_authenticator(None) { - Ok(success) => { - if success { - println!("Removed authenticator from {}", account.account_name); - successful.push(account.account_name.clone()); - } 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()); - } - _ => {} - } - } - } - Err(err) => { - error!( - "Unexpected error when removing authenticator from {}: {}", - account.account_name, err - ); - } - } - } - - for account_name in successful { - manifest.remove_account(account_name); - } - - manifest.save()?; - Ok(()) -} - -fn do_subcmd_encrypt( - _args: cli::ArgsEncrypt, - manifest: &mut accountmanager::AccountManager, -) -> anyhow::Result<()> { - if !manifest.has_passkey() { - let mut passkey; - loop { - passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); - if let Some(p) = passkey.as_ref() { - if p.is_empty() { - error!("Passkey cannot be empty, try again."); - continue; - } - } - let passkey_confirm = - rpassword::prompt_password_stdout("Confirm encryption passkey: ").ok(); - if passkey == passkey_confirm { - break; - } - error!("Passkeys do not match, try again."); - } - manifest.submit_passkey(passkey); - } - manifest.load_accounts()?; - for entry in manifest.iter_mut() { - entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); - } - manifest.save()?; - Ok(()) -} - -fn do_subcmd_decrypt( - _args: cli::ArgsDecrypt, - manifest: &mut accountmanager::AccountManager, -) -> anyhow::Result<()> { - load_accounts_with_prompts(manifest)?; - for mut entry in manifest.iter_mut() { - entry.encryption = None; - } - manifest.submit_passkey(None); - manifest.save()?; - Ok(()) -} - -fn do_subcmd_code( - args: cli::ArgsCode, - selected_accounts: Vec>>, -) -> anyhow::Result<()> { - let server_time = if args.offline { - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() - } else { - steamapi::get_server_time()?.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); - } - Ok(()) -} - -#[cfg(feature = "qr")] -fn do_subcmd_qr( - args: cli::ArgsQr, - selected_accounts: Vec>>, -) -> anyhow::Result<()> { - use anyhow::Context; - - info!( - "Generating QR codes for {} accounts", - selected_accounts.len() - ); - - for account in selected_accounts { - let account = account.lock().unwrap(); - let qr = QrCode::new(account.uri.expose_secret()) - .context(format!("generating qr code for {}", account.account_name))?; - - info!("Printing QR code for {}", account.account_name); - let qr_string = if args.ascii { - qr.render() - .light_color(' ') - .dark_color('#') - .module_dimensions(2, 1) - .build() - } else { - use qrcode::render::unicode; - qr.render::() - .dark_color(unicode::Dense1x2::Light) - .light_color(unicode::Dense1x2::Dark) - .build() - }; - - println!("{}", qr_string); - } - Ok(()) -} diff --git a/src/secret_string.rs b/src/secret_string.rs index 3398fef..6957f14 100644 --- a/src/secret_string.rs +++ b/src/secret_string.rs @@ -1,13 +1,5 @@ -use secrecy::{ExposeSecret, SecretString}; -use serde::{Deserialize, Deserializer, Serializer}; - -/// Helper to allow serializing a [secrecy::SecretString] as a [String] -pub(crate) fn serialize(secret_string: &SecretString, serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_str(secret_string.expose_secret()) -} +use secrecy::SecretString; +use serde::{Deserialize, Deserializer}; /// Helper to allow deserializing a [String] as a [secrecy::SecretString] pub(crate) fn deserialize<'de, D>(d: D) -> Result @@ -20,35 +12,18 @@ where #[cfg(test)] mod test { - use serde::Serialize; + use secrecy::ExposeSecret; use super::*; - #[test] - fn test_secret_string_round_trip() { - #[derive(Serialize, Deserialize)] - struct Foo { - #[serde(with = "super")] - secret: SecretString, - } - - let foo = Foo { - secret: String::from("hello").into(), - }; - - let s = serde_json::to_string(&foo).unwrap(); - let foo2: Foo = serde_json::from_str(&s).unwrap(); - assert_eq!(foo.secret.expose_secret(), foo2.secret.expose_secret()); + #[derive(Deserialize)] + struct Foo { + #[serde(with = "super")] + secret: SecretString, } #[test] fn test_secret_string_deserialize() { - #[derive(Serialize, Deserialize)] - struct Foo { - #[serde(with = "super")] - secret: SecretString, - } - let foo: Foo = serde_json::from_str("{\"secret\": \"hello\"}").unwrap(); assert_eq!(foo.secret.expose_secret(), "hello"); } diff --git a/src/test_login.rs b/src/test_login.rs deleted file mode 100644 index 997c6f2..0000000 --- a/src/test_login.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use log::info; - -use steamguard::SteamGuardAccount; - -use crate::do_login; - -pub fn do_subcmd_test_login( - selected_accounts: Vec>>, -) -> anyhow::Result<()> { - for account in selected_accounts { - let mut account = account.lock().unwrap(); - do_login(&mut account)?; - info!("Logged in successfully!"); - } - Ok(()) -} diff --git a/src/tui.rs b/src/tui.rs index df63e43..68c9804 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -6,39 +6,10 @@ use crossterm::{ terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, QueueableCommand, }; -use log::*; -use regex::Regex; use std::collections::HashSet; use std::io::{stderr, stdout, Write}; use steamguard::Confirmation; -lazy_static! { - static ref CAPTCHA_VALID_CHARS: Regex = - Regex::new("^([A-H]|[J-N]|[P-R]|[T-Z]|[2-4]|[7-9]|[@%&])+$").unwrap(); -} - -pub fn validate_captcha_text(text: &String) -> bool { - CAPTCHA_VALID_CHARS.is_match(text) -} - -#[test] -fn test_validate_captcha_text() { - assert!(validate_captcha_text(&String::from("2WWUA@"))); - assert!(validate_captcha_text(&String::from("3G8HT2"))); - assert!(validate_captcha_text(&String::from("3J%@X3"))); - assert!(validate_captcha_text(&String::from("2GCZ4A"))); - assert!(validate_captcha_text(&String::from("3G8HT2"))); - assert!(!validate_captcha_text(&String::from("asd823"))); - assert!(!validate_captcha_text(&String::from("!PQ4RD"))); - assert!(!validate_captcha_text(&String::from("1GQ4XZ"))); - assert!(!validate_captcha_text(&String::from("8GO4XZ"))); - assert!(!validate_captcha_text(&String::from("IPQ4RD"))); - assert!(!validate_captcha_text(&String::from("0PT4RD"))); - assert!(!validate_captcha_text(&String::from("APTSRD"))); - assert!(!validate_captcha_text(&String::from("AP5TRD"))); - assert!(!validate_captcha_text(&String::from("AP6TRD"))); -} - /// Prompt the user for text input. pub(crate) fn prompt() -> String { stdout().flush().expect("failed to flush stdout"); @@ -60,20 +31,6 @@ pub(crate) fn prompt() -> String { line } -pub(crate) fn prompt_captcha_text(captcha_gid: &String) -> String { - eprintln!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid); - let mut captcha_text; - loop { - eprint!("Enter captcha text: "); - captcha_text = prompt(); - if !captcha_text.is_empty() && validate_captcha_text(&captcha_text) { - break; - } - warn!("Invalid chars for captcha text found in user's input. Prompting again..."); - } - captcha_text -} - /// Prompt the user for a single character response. Useful for asking yes or no questions. /// /// `chars` should be all lowercase characters, with at most 1 uppercase character. The uppercase character is the default answer if no answer is provided. @@ -149,7 +106,7 @@ pub(crate) fn prompt_confirmation_menu( ), )?; - for i in 0..confirmations.len() { + for (i, conf) in confirmations.iter().enumerate() { stdout().queue(Print("\r"))?; if selected_idx == i { stdout().queue(SetForegroundColor(Color::Yellow))?; @@ -173,7 +130,7 @@ pub(crate) fn prompt_confirmation_menu( stdout().queue(SetForegroundColor(Color::Yellow))?; } - stdout().queue(Print(format!(" {}\n", confirmations[i].description())))?; + stdout().queue(Print(format!(" {}\n", conf.description())))?; } stdout().flush()?; diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 1b0a1bf..9e18988 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -4,11 +4,7 @@ use crate::protobufs::service_twofactor::{ use crate::steamapi::twofactor::TwoFactorClient; use crate::token::TwoFactorSecret; use crate::transport::WebApiTransport; -use crate::{ - steamapi::{EResult, Session, SteamApiClient}, - token::Tokens, - SteamGuardAccount, -}; +use crate::{steamapi::EResult, token::Tokens, SteamGuardAccount}; use log::*; use thiserror::Error; @@ -81,14 +77,14 @@ impl AccountLinker { revocation_code: resp.take_revocation_code().into(), uri: resp.take_uri().into(), shared_secret: TwoFactorSecret::from_bytes(resp.take_shared_secret()), - token_gid: resp.take_token_gid().into(), - identity_secret: base64::encode(&resp.take_identity_secret()).into(), + token_gid: resp.take_token_gid(), + identity_secret: base64::encode(resp.take_identity_secret()).into(), device_id: self.device_id.clone(), - secret_1: base64::encode(&resp.take_secret_1()).into(), + secret_1: base64::encode(resp.take_secret_1()).into(), tokens: Some(self.tokens.clone()), }; let success = AccountLinkSuccess { - account: account, + account, server_time: resp.server_time(), phone_number_hint: resp.take_phone_number_hint(), }; @@ -128,7 +124,7 @@ impl AccountLinker { } self.finalized = true; - return Ok(()); + Ok(()) } } @@ -158,7 +154,7 @@ impl AccountLinkSuccess { } fn generate_device_id() -> String { - return format!("android:{}", uuid::Uuid::new_v4().to_string()); + format!("android:{}", uuid::Uuid::new_v4()) } #[derive(Error, Debug)] diff --git a/steamguard/src/api_responses/i_two_factor_service.rs b/steamguard/src/api_responses/i_two_factor_service.rs deleted file mode 100644 index ca9358a..0000000 --- a/steamguard/src/api_responses/i_two_factor_service.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::{token::TwoFactorSecret, SteamGuardAccount}; - -use super::parse_json_string_as_number; -use serde::{Deserialize, Serialize}; - -/// Represents the response from `/ITwoFactorService/QueryTime/v0001` -#[deprecated] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QueryTimeResponse { - /// The time that the server will use to check your two factor code. - #[serde(deserialize_with = "parse_json_string_as_number")] - pub server_time: u64, - #[serde(deserialize_with = "parse_json_string_as_number")] - pub skew_tolerance_seconds: u64, - #[serde(deserialize_with = "parse_json_string_as_number")] - pub large_time_jink: u64, - pub probe_frequency_seconds: u64, - pub adjusted_time_probe_frequency_seconds: u64, - pub hint_probe_frequency_seconds: u64, - pub sync_timeout: u64, - pub try_again_seconds: u64, - pub max_attempts: u64, -} - -#[deprecated] -#[derive(Debug, Clone, Deserialize)] -pub struct AddAuthenticatorResponse { - /// Shared secret between server and authenticator - #[serde(default)] - pub shared_secret: String, - /// Authenticator serial number (unique per token) - #[serde(default)] - pub serial_number: String, - /// code used to revoke authenticator - #[serde(default)] - pub revocation_code: String, - /// URI for QR code generation - #[serde(default)] - pub uri: String, - /// Current server time - #[serde(default, deserialize_with = "parse_json_string_as_number")] - pub server_time: u64, - /// Account name to display on token client - #[serde(default)] - pub account_name: String, - /// Token GID assigned by server - #[serde(default)] - pub token_gid: String, - /// Secret used for identity attestation (e.g., for eventing) - #[serde(default)] - pub identity_secret: String, - /// Spare shared secret - #[serde(default)] - pub secret_1: String, - /// Result code - pub status: i32, - #[serde(default)] - pub phone_number_hint: Option, -} - -#[deprecated] -#[derive(Debug, Clone, Deserialize)] -pub struct FinalizeAddAuthenticatorResponse { - pub status: i32, - #[serde(deserialize_with = "parse_json_string_as_number")] - pub server_time: u64, - pub want_more: bool, - pub success: bool, -} - -#[deprecated] -#[derive(Debug, Clone, Deserialize)] -pub struct RemoveAuthenticatorResponse { - pub success: bool, -} - -#[cfg(test)] -mod test { - use super::*; - use crate::api_responses::SteamApiResponse; - - #[test] - fn test_parse_add_auth_response() { - let result = serde_json::from_str::>( - include_str!("../fixtures/api-responses/add-authenticator-1.json"), - ); - - assert!( - matches!(result, Ok(_)), - "got error: {}", - result.unwrap_err() - ); - let resp = result.unwrap().response; - - assert_eq!(resp.server_time, 1628559846); - assert_eq!(resp.shared_secret, "wGwZx=sX5MmTxi6QgA3Gi"); - assert_eq!(resp.revocation_code, "R123456"); - } - - #[test] - fn test_parse_add_auth_response2() { - let result = serde_json::from_str::>( - include_str!("../fixtures/api-responses/add-authenticator-2.json"), - ); - - assert!( - matches!(result, Ok(_)), - "got error: {}", - result.unwrap_err() - ); - let resp = result.unwrap().response; - - assert_eq!(resp.status, 29); - } -} diff --git a/steamguard/src/api_responses/login.rs b/steamguard/src/api_responses/login.rs index ffa58ad..9f1fcb3 100644 --- a/steamguard/src/api_responses/login.rs +++ b/steamguard/src/api_responses/login.rs @@ -1,15 +1,4 @@ -use serde::{Deserialize, Deserializer, Serialize}; - -use super::parse_json_string_as_number; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LoginTransferParameters { - pub steamid: String, - pub token_secure: String, - pub auth: String, - pub remember_login: bool, - pub webcookie: String, -} +use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct OAuthData { @@ -21,57 +10,6 @@ pub struct OAuthData { pub webcookie: String, } -#[derive(Debug, Clone, Deserialize)] -#[deprecated] -pub struct RsaResponse { - pub success: bool, - pub publickey_exp: String, - pub publickey_mod: String, - pub timestamp: String, - pub token_gid: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct LoginResponse { - pub success: bool, - #[serde(default)] - pub login_complete: bool, - #[serde(default)] - pub captcha_needed: bool, - #[serde(default)] - pub captcha_gid: String, - #[serde(default, deserialize_with = "parse_json_string_as_number")] - pub emailsteamid: u64, - #[serde(default)] - pub emailauth_needed: bool, - #[serde(default)] - pub requires_twofactor: bool, - #[serde(default)] - pub message: String, - #[serde(default, deserialize_with = "oauth_data_from_string")] - pub oauth: Option, - pub transfer_urls: Option>, - pub transfer_parameters: Option, -} - -/// For some reason, the `oauth` field in the login response is a string of JSON, not a JSON object. -/// Deserializes to `Option` because the `oauth` field is not always there. -fn oauth_data_from_string<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - // for some reason, deserializing to &str doesn't work but this does. - let s: String = Deserialize::deserialize(deserializer)?; - let data: OAuthData = serde_json::from_str(s.as_str()).map_err(serde::de::Error::custom)?; - Ok(Some(data)) -} - -impl LoginResponse { - pub fn needs_transfer_login(&self) -> bool { - self.transfer_urls.is_some() || self.transfer_parameters.is_some() - } -} - #[cfg(test)] mod test { use super::*; @@ -90,49 +28,4 @@ mod test { ); assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5"); } - - #[test] - fn test_login_response_parse() { - let result = serde_json::from_str::(include_str!( - "../fixtures/api-responses/login-response1.json" - )); - - assert!( - matches!(result, Ok(_)), - "got error: {}", - result.unwrap_err() - ); - let resp = result.unwrap(); - - let oauth = resp.oauth.unwrap(); - assert_eq!(oauth.steamid, "78562647129469312"); - assert_eq!(oauth.oauth_token, "fd2fdb3d0717bad2220d98c7ec61c7bd"); - assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732"); - assert_eq!( - oauth.wgtoken_secure, - "21061EA13C36D7C29812CAED900A215171AD13A2" - ); - assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5"); - } - - #[test] - fn test_login_response_parse_missing_webcookie() { - let result = serde_json::from_str::(include_str!( - "../fixtures/api-responses/login-response-missing-webcookie.json" - )); - - assert!( - matches!(result, Ok(_)), - "got error: {}", - result.unwrap_err() - ); - let resp = result.unwrap(); - - let oauth = resp.oauth.unwrap(); - assert_eq!(oauth.steamid, "92591609556178617"); - assert_eq!(oauth.oauth_token, "1cc83205dab2979e558534dab29f6f3aa"); - assert_eq!(oauth.wgtoken, "3EDA9DEF07D7B39361D95203525D8AFE82A"); - assert_eq!(oauth.wgtoken_secure, "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48"); - assert_eq!(oauth.webcookie, ""); - } } diff --git a/steamguard/src/api_responses/mod.rs b/steamguard/src/api_responses/mod.rs index e832d2a..00796b9 100644 --- a/steamguard/src/api_responses/mod.rs +++ b/steamguard/src/api_responses/mod.rs @@ -1,25 +1,7 @@ mod i_authentication_service; -mod i_two_factor_service; mod login; mod phone_ajax; pub use i_authentication_service::*; -pub use i_two_factor_service::*; pub use login::*; pub use phone_ajax::*; - -use serde::{Deserialize, Deserializer}; - -pub(crate) fn parse_json_string_as_number<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - // for some reason, deserializing to &str doesn't work but this does. - let s: String = Deserialize::deserialize(deserializer)?; - Ok(s.parse().unwrap()) -} - -#[derive(Debug, Clone, Deserialize)] -pub struct SteamApiResponse { - pub response: T, -} diff --git a/steamguard/src/fixtures/api-responses/login-response-missing-webcookie.json b/steamguard/src/fixtures/api-responses/login-response-missing-webcookie.json deleted file mode 100644 index 94e933d..0000000 --- a/steamguard/src/fixtures/api-responses/login-response-missing-webcookie.json +++ /dev/null @@ -1 +0,0 @@ -{"success":true,"requires_twofactor":false,"redirect_uri":"steammobile:\/\/mobileloginsucceeded","login_complete":true,"oauth":"{\"steamid\":\"92591609556178617\",\"account_name\":\"hydrastar2\",\"oauth_token\":\"1cc83205dab2979e558534dab29f6f3aa\",\"wgtoken\":\"3EDA9DEF07D7B39361D95203525D8AFE82A\",\"wgtoken_secure\":\"F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48\"}"} \ No newline at end of file diff --git a/steamguard/src/fixtures/api-responses/login-response1.json b/steamguard/src/fixtures/api-responses/login-response1.json deleted file mode 100644 index b16eb66..0000000 --- a/steamguard/src/fixtures/api-responses/login-response1.json +++ /dev/null @@ -1 +0,0 @@ -{"success":true,"requires_twofactor":false,"redirect_uri":"steammobile://mobileloginsucceeded","login_complete":true,"oauth":"{\"steamid\":\"78562647129469312\",\"account_name\":\"feuarus\",\"oauth_token\":\"fd2fdb3d0717bad2220d98c7ec61c7bd\",\"wgtoken\":\"72E7013D598A4F68C7E268F6FA3767D89D763732\",\"wgtoken_secure\":\"21061EA13C36D7C29812CAED900A215171AD13A2\",\"webcookie\":\"6298070A226E5DAD49938D78BCF36F7A7118FDD5\"}"} \ No newline at end of file diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index d3609f1..8200292 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -1,8 +1,5 @@ -use crate::api_responses::SteamApiResponse; use crate::confirmation::{ConfirmationListResponse, SendConfirmationResponse}; -use crate::protobufs::service_twofactor::{ - CTwoFactor_RemoveAuthenticator_Request, CTwoFactor_RemoveAuthenticator_Response, -}; +use crate::protobufs::service_twofactor::CTwoFactor_RemoveAuthenticator_Request; use crate::steamapi::EResult; use crate::{ steamapi::twofactor::TwoFactorClient, token::TwoFactorSecret, transport::WebApiTransport, @@ -17,11 +14,9 @@ use reqwest::{ header::{COOKIE, USER_AGENT}, Url, }; -use scraper::{Html, Selector}; pub use secrecy::{ExposeSecret, SecretString}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, convert::TryInto, io::Read}; -use steamapi::SteamApiClient; +use std::{collections::HashMap, io::Read}; use token::Tokens; pub use userlogin::{DeviceDetails, LoginError, UserLogin}; @@ -29,7 +24,6 @@ pub use userlogin::{DeviceDetails, LoginError, UserLogin}; extern crate lazy_static; #[macro_use] extern crate anyhow; -#[macro_use] extern crate maplit; pub mod accountlinker; @@ -66,22 +60,25 @@ pub struct SteamGuardAccount { } fn build_time_bytes(time: u64) -> [u8; 8] { - return time.to_be_bytes(); + time.to_be_bytes() } -fn generate_confirmation_hash_for_time(time: u64, tag: &str, identity_secret: &String) -> String { - let decode: &[u8] = &base64::decode(&identity_secret).unwrap(); +fn generate_confirmation_hash_for_time( + time: u64, + tag: &str, + identity_secret: impl AsRef<[u8]>, +) -> String { + let decode: &[u8] = &base64::decode(identity_secret).unwrap(); let time_bytes = build_time_bytes(time); let tag_bytes = tag.as_bytes(); let array = [&time_bytes, tag_bytes].concat(); let hash = hmac_sha1(decode, &array); - let encoded = base64::encode(hash); - return encoded; + base64::encode(hash) } -impl SteamGuardAccount { - pub fn new() -> Self { - return SteamGuardAccount { +impl Default for SteamGuardAccount { + fn default() -> Self { + Self { account_name: String::from(""), steam_id: 0, serial_number: String::from(""), @@ -93,7 +90,13 @@ impl SteamGuardAccount { device_id: String::from(""), secret_1: String::from("").into(), tokens: None, - }; + } + } +} + +impl SteamGuardAccount { + pub fn new() -> Self { + Self::default() } pub fn from_reader(r: T) -> anyhow::Result @@ -108,11 +111,11 @@ impl SteamGuardAccount { } pub fn is_logged_in(&self) -> bool { - return self.tokens.is_some(); + self.tokens.is_some() } pub fn generate_code(&self, time: u64) -> String { - return self.shared_secret.generate_code(time); + self.shared_secret.generate_code(time) } fn get_confirmation_query_params(&self, tag: &str, time: u64) -> HashMap<&str, String> { @@ -121,12 +124,12 @@ impl SteamGuardAccount { params.insert("a", self.steam_id.to_string()); params.insert( "k", - generate_confirmation_hash_for_time(time, tag, &self.identity_secret.expose_secret()), + generate_confirmation_hash_for_time(time, tag, self.identity_secret.expose_secret()), ); params.insert("t", time.to_string()); params.insert("m", String::from("android")); params.insert("tag", String::from(tag)); - return params; + params } fn build_cookie_jar(&self) -> reqwest::cookie::Jar { @@ -149,7 +152,7 @@ impl SteamGuardAccount { .as_str(), &url, ); - return cookies; + cookies } pub fn get_trade_confirmations(&self) -> Result, anyhow::Error> { @@ -161,7 +164,7 @@ impl SteamGuardAccount { .cookie_store(true) .build()?; - let time = steamapi::get_server_time()?.server_time; + let time = steamapi::get_server_time()?.server_time(); let resp = client .get("https://steamcommunity.com/mobileconf/getlist".parse::().unwrap()) .header("X-Requested-With", "com.valvesoftware.android.steam.community") @@ -193,7 +196,7 @@ impl SteamGuardAccount { .cookie_store(true) .build()?; - let time = steamapi::get_server_time()?.server_time; + let time = steamapi::get_server_time()?.server_time(); let mut query_params = self.get_confirmation_query_params("conf", time); query_params.insert("op", operation); query_params.insert("cid", conf.id.to_string()); @@ -246,7 +249,7 @@ impl SteamGuardAccount { .cookie_store(true) .build()?; - let time = steamapi::get_server_time()?.server_time; + let time = steamapi::get_server_time()?.server_time(); let query_params = self.get_confirmation_query_params("details", time); let resp: ConfirmationDetailsResponse = client.get(format!("https://steamcommunity.com/mobileconf/details/{}", conf.id).parse::().unwrap()) @@ -292,11 +295,7 @@ mod tests { #[test] fn test_generate_confirmation_hash_for_time() { assert_eq!( - generate_confirmation_hash_for_time( - 1617591917, - "conf", - &String::from("GQP46b73Ws7gr8GmZFR0sDuau5c=") - ), + generate_confirmation_hash_for_time(1617591917, "conf", "GQP46b73Ws7gr8GmZFR0sDuau5c="), String::from("NaL8EIMhfy/7vBounJ0CvpKbrPk=") ); } diff --git a/steamguard/src/protobufs.rs b/steamguard/src/protobufs.rs index fb172a5..a8baa3b 100644 --- a/steamguard/src/protobufs.rs +++ b/steamguard/src/protobufs.rs @@ -1,11 +1,3 @@ -use std::fmt::Formatter; -use std::marker::PhantomData; - -use protobuf::EnumFull; -use protobuf::EnumOrUnknown; -use protobuf::MessageField; -use serde::{Deserialize, Serialize}; - include!(concat!(env!("OUT_DIR"), "/protobufs/mod.rs")); #[cfg(test)] diff --git a/steamguard/src/secret_string.rs b/steamguard/src/secret_string.rs index 02361e5..cb840ee 100644 --- a/steamguard/src/secret_string.rs +++ b/steamguard/src/secret_string.rs @@ -1,5 +1,5 @@ use secrecy::{ExposeSecret, SecretString}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serializer}; /// Helper to allow serializing a [secrecy::SecretString] as a [String] pub(crate) fn serialize(secret_string: &SecretString, serializer: S) -> Result @@ -20,16 +20,18 @@ where #[cfg(test)] mod test { + use serde::Serialize; + use super::*; + #[derive(Serialize, Deserialize)] + struct Foo { + #[serde(with = "super")] + secret: SecretString, + } + #[test] fn test_secret_string_round_trip() { - #[derive(Serialize, Deserialize)] - struct Foo { - #[serde(with = "super")] - secret: SecretString, - } - let foo = Foo { secret: String::from("hello").into(), }; @@ -41,12 +43,6 @@ mod test { #[test] fn test_secret_string_deserialize() { - #[derive(Serialize, Deserialize)] - struct Foo { - #[serde(with = "super")] - secret: SecretString, - } - let foo: Foo = serde_json::from_str("{\"secret\": \"hello\"}").unwrap(); assert_eq!(foo.secret.expose_secret(), "hello"); } diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index d3350ad..8f24817 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -1,482 +1,31 @@ pub mod authentication; pub mod twofactor; -use crate::{api_responses::*, token::Jwt}; -use log::*; -use reqwest::{ - blocking::RequestBuilder, - cookie::CookieStore, - header::COOKIE, - header::{HeaderMap, HeaderName, HeaderValue, SET_COOKIE}, - Url, +use crate::{ + protobufs::service_twofactor::CTwoFactor_Time_Response, token::Jwt, transport::WebApiTransport, }; -use secrecy::{CloneableSecret, DebugSecret, ExposeSecret, SerializableSecret}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::iter::FromIterator; -use std::str::FromStr; -use std::time::{SystemTime, UNIX_EPOCH}; -use zeroize::Zeroize; +use reqwest::Url; +use serde::Deserialize; + +pub use self::authentication::AuthenticationClient; +pub use self::twofactor::TwoFactorClient; lazy_static! { static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::().unwrap(); static ref STEAM_API_BASE: String = "https://api.steampowered.com".into(); } -#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)] -#[zeroize(drop)] -pub struct Session { - #[serde(rename = "SessionID")] - pub session_id: String, - #[serde(rename = "SteamLogin")] - pub steam_login: String, - #[serde(rename = "SteamLoginSecure")] - pub steam_login_secure: String, - #[serde(default, rename = "WebCookie")] - pub web_cookie: Option, - #[serde(rename = "OAuthToken")] - pub token: String, - #[serde(rename = "SteamID")] - pub steam_id: u64, -} - -impl SerializableSecret for Session {} -impl CloneableSecret for Session {} -impl DebugSecret for Session {} - -/// Queries Steam for the current time. +/// Queries Steam for the current time. A convenience function around TwoFactorClient. /// /// Endpoint: `/ITwoFactorService/QueryTime/v0001` -/// -/// Example Response: -/// ```json -/// { -/// "response": { -/// "server_time": "1655768666", -/// "skew_tolerance_seconds": "60", -/// "large_time_jink": "86400", -/// "probe_frequency_seconds": 3600, -/// "adjusted_time_probe_frequency_seconds": 300, -/// "hint_probe_frequency_seconds": 60, -/// "sync_timeout": 60, -/// "try_again_seconds": 900, -/// "max_attempts": 3 -/// } -/// } -/// ``` -pub fn get_server_time() -> anyhow::Result { - let client = reqwest::blocking::Client::new(); - let resp = client - .post("https://api.steampowered.com/ITwoFactorService/QueryTime/v0001") - .body("steamid=0") - .send()?; - let resp: SteamApiResponse = resp.json()?; - - return Ok(resp.response); -} - -/// Provides raw access to the Steam API. Handles cookies, some deserialization, etc. to make it easier. It covers `ITwoFactorService` from the Steam web API, and some mobile app specific api endpoints. -#[derive(Debug)] -pub struct SteamApiClient { - cookies: reqwest::cookie::Jar, - client: reqwest::blocking::Client, - pub session: Option>, -} - -impl SteamApiClient { - pub fn new(session: Option>) -> SteamApiClient { - SteamApiClient { - cookies: reqwest::cookie::Jar::default(), - client: reqwest::blocking::ClientBuilder::new() - .cookie_store(true) - .user_agent("Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30") - .default_headers(HeaderMap::from_iter(hashmap! { - HeaderName::from_str("X-Requested-With").expect("could not build default request headers") => HeaderValue::from_str("com.valvesoftware.android.steam.community").expect("could not build default request headers") - }.into_iter())) - .build() - .unwrap(), - session: session, - } +pub fn get_server_time() -> anyhow::Result { + let mut client = TwoFactorClient::new(WebApiTransport::new()); + let resp = client.query_time()?; + if resp.result != EResult::OK { + return Err(anyhow::anyhow!("QueryTime failed: {:?}", resp)); } - fn build_session(&self, data: &OAuthData) -> Session { - trace!("SteamApiClient::build_session"); - return Session { - token: data.oauth_token.clone(), - steam_id: data.steamid.parse().unwrap(), - steam_login: format!("{}%7C%7C{}", data.steamid, data.wgtoken), - steam_login_secure: format!("{}%7C%7C{}", data.steamid, data.wgtoken_secure), - session_id: self - .extract_session_id() - .expect("failed to extract session id from cookies"), - web_cookie: Some(data.webcookie.clone()), - }; - } - - fn extract_session_id(&self) -> Option { - let cookies = self.cookies.cookies(&STEAM_COOKIE_URL).unwrap(); - let all_cookies = cookies.to_str().unwrap(); - for cookie in all_cookies - .split(";") - .map(|s| cookie::Cookie::parse(s).unwrap()) - { - if cookie.name() == "sessionid" { - return Some(cookie.value().into()); - } - } - return None; - } - - pub fn save_cookies_from_response(&mut self, response: &reqwest::blocking::Response) { - let set_cookie_iter = response.headers().get_all(SET_COOKIE); - - for c in set_cookie_iter { - c.to_str() - .into_iter() - .for_each(|cookie_str| self.cookies.add_cookie_str(cookie_str, &STEAM_COOKIE_URL)); - } - } - - pub fn request( - &self, - method: reqwest::Method, - url: U, - ) -> RequestBuilder { - trace!("making request: {} {}", method, url); - self.cookies - .add_cookie_str("mobileClientVersion=0 (2.1.3)", &STEAM_COOKIE_URL); - self.cookies - .add_cookie_str("mobileClient=android", &STEAM_COOKIE_URL); - self.cookies - .add_cookie_str("Steam_Language=english", &STEAM_COOKIE_URL); - if let Some(session) = &self.session { - self.cookies.add_cookie_str( - format!("sessionid={}", session.expose_secret().session_id).as_str(), - &STEAM_COOKIE_URL, - ); - } - - self.client - .request(method, url) - .header(COOKIE, self.cookies.cookies(&STEAM_COOKIE_URL).unwrap()) - } - - pub fn get(&self, url: U) -> RequestBuilder { - self.request(reqwest::Method::GET, url) - } - - pub fn post(&self, url: U) -> RequestBuilder { - self.request(reqwest::Method::POST, url) - } - - /// Updates the cookie jar with the session cookies by pinging steam servers. - pub fn update_session(&mut self) -> anyhow::Result<()> { - trace!("SteamApiClient::update_session"); - - let resp = self - .get("https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client".parse::().unwrap()) - .send()?; - self.save_cookies_from_response(&resp); - trace!("{:?}", resp); - - trace!("cookies: {:?}", self.cookies); - Ok(()) - } - - /// Endpoint: POST /login/dologin - pub fn login( - &mut self, - username: String, - encrypted_password: String, - twofactor_code: String, - email_code: String, - captcha_gid: String, - captcha_text: String, - rsa_timestamp: String, - ) -> anyhow::Result { - let params = hashmap! { - "donotcache" => format!( - "{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - * 1000 - ), - "username" => username, - "password" => encrypted_password, - "twofactorcode" => twofactor_code, - "emailauth" => email_code, - "captchagid" => captcha_gid, - "captcha_text" => captcha_text, - "rsatimestamp" => rsa_timestamp, - "remember_login" => "true".into(), - "oauth_client_id" => "DE45CD61".into(), - "oauth_scope" => "read_profile write_profile read_client write_client".into(), - }; - - let resp = self - .post("https://steamcommunity.com/login/dologin") - .form(¶ms) - .send()?; - self.save_cookies_from_response(&resp); - let text = resp.text()?; - trace!("raw login response: {}", text); - - let login_resp: LoginResponse = serde_json::from_str(text.as_str())?; - - if let Some(oauth) = &login_resp.oauth { - self.session = Some(secrecy::Secret::new(self.build_session(&oauth))); - } - - return Ok(login_resp); - } - - /// A secondary step in the login flow. Does not seem to always be needed? - /// Endpoints: provided by `login()` - pub fn transfer_login(&mut self, login_resp: LoginResponse) -> anyhow::Result { - match (login_resp.transfer_urls, login_resp.transfer_parameters) { - (Some(urls), Some(params)) => { - debug!("received transfer parameters, relaying data..."); - for url in urls { - trace!("posting transfer to {}", url); - let resp = self.client.post(url).json(¶ms).send()?; - self.save_cookies_from_response(&resp); - } - - let oauth = OAuthData { - oauth_token: params.auth, - steamid: params.steamid.parse().unwrap(), - wgtoken: params.token_secure.clone(), // guessing - wgtoken_secure: params.token_secure, - webcookie: params.webcookie, - }; - self.session = Some(secrecy::Secret::new(self.build_session(&oauth))); - return Ok(oauth); - } - (None, None) => { - bail!("did not receive transfer_urls and transfer_parameters"); - } - (_, None) => { - bail!("did not receive transfer_parameters"); - } - (None, _) => { - bail!("did not receive transfer_urls"); - } - } - } - - /// Likely removed now - /// - /// One of the endpoints that handles phone number things. Can check to see if phone is present on account, and maybe do some other stuff. It's not really super clear. - /// - /// Host: steamcommunity.com - /// Endpoint: POST /steamguard/phoneajax - /// Requires `sessionid` cookie to be set. - fn phoneajax(&self, op: &str, arg: &str) -> anyhow::Result { - let mut params = hashmap! { - "op" => op, - "arg" => arg, - "sessionid" => self.session.as_ref().unwrap().expose_secret().session_id.as_str(), - }; - if op == "check_sms_code" { - params.insert("checkfortos", "0"); - params.insert("skipvoip", "1"); - } - - let resp = self - .post("https://steamcommunity.com/steamguard/phoneajax") - .form(¶ms) - .send()?; - - trace!("phoneajax: status={}", resp.status()); - let result: Value = resp.json()?; - trace!("phoneajax: {:?}", result); - if result["has_phone"] != Value::Null { - trace!("op: {} - found has_phone field", op); - return result["has_phone"] - .as_bool() - .ok_or(anyhow!("failed to parse has_phone field into boolean")); - } else if result["success"] != Value::Null { - trace!("op: {} - found success field", op); - return result["success"] - .as_bool() - .ok_or(anyhow!("failed to parse success field into boolean")); - } else { - trace!("op: {} - did not find any expected field", op); - return Ok(false); - } - } - - /// Works similar to phoneajax. Used in the process to add a phone number to a steam account. - /// Valid ops: - /// - get_phone_number => `input` is treated as a phone number to add to the account. Yes, this is somewhat counter intuitive. - /// - resend_sms - /// - get_sms_code => `input` is treated as a the SMS code that was texted to the phone number. Again, this is somewhat counter intuitive. After this succeeds, the phone number is added to the account. - /// - email_verification => If the account is protected with steam guard email, a verification link is sent. After the link in the email is clicked, send this op. After, an SMS code is sent to the phone number. - /// - retry_email_verification - /// - /// Host: store.steampowered.com - /// Endpoint: /phone/add_ajaxop - fn phone_add_ajaxop(&self, op: &str, input: &str) -> anyhow::Result<()> { - trace!("phone_add_ajaxop: op={} input={}", op, input); - let params = hashmap! { - "op" => op, - "input" => input, - "sessionid" => self.session.as_ref().unwrap().expose_secret().session_id.as_str(), - }; - - let resp = self - .post("https://store.steampowered.com/phone/add_ajaxop") - .form(¶ms) - .send()?; - trace!("phone_add_ajaxop: http status={}", resp.status()); - let text = resp.text()?; - trace!("phone_add_ajaxop response: {}", text); - - todo!(); - } - - pub fn has_phone(&self) -> anyhow::Result { - return self.phoneajax("has_phone", "null"); - } - - pub fn check_sms_code(&self, sms_code: String) -> anyhow::Result { - return self.phoneajax("check_sms_code", sms_code.as_str()); - } - - pub fn check_email_confirmation(&self) -> anyhow::Result { - return self.phoneajax("email_confirmation", ""); - } - - pub fn add_phone_number(&self, phone_number: String) -> anyhow::Result { - // return self.phoneajax("add_phone_number", phone_number.as_str()); - todo!(); - } - - /// Provides lots of juicy information, like if the number is a VOIP number. - /// Host: store.steampowered.com - /// Endpoint: POST /phone/validate - /// Body format: form data - /// Example: - /// ```form - /// sessionID=FOO&phoneNumber=%2B1+1234567890 - /// ``` - /// Found on page: https://store.steampowered.com/phone/add - pub fn phone_validate(&self, phone_number: &String) -> anyhow::Result { - let params = hashmap! { - "sessionID" => self.session.as_ref().unwrap().expose_secret().session_id.as_str(), - "phoneNumber" => phone_number.as_str(), - }; - - let resp = self - .client - .post("https://store.steampowered.com/phone/validate") - .form(¶ms) - .send()? - .json::()?; - - return Ok(resp); - } - - /// Starts the authenticator linking process. - /// This doesn't check any prereqisites to ensure the request will pass validation on Steam's side (eg. sms/email confirmations). - /// A valid `Session` is required for this request. Cookies are not needed for this request, but they are set anyway. - /// - /// Host: api.steampowered.com - /// Endpoint: POST /ITwoFactorService/AddAuthenticator/v0001 - pub fn add_authenticator( - &mut self, - device_id: String, - ) -> anyhow::Result { - ensure!(matches!(self.session, Some(_))); - let params = hashmap! { - "access_token" => self.session.as_ref().unwrap().expose_secret().token.clone(), - "steamid" => self.session.as_ref().unwrap().expose_secret().steam_id.to_string(), - "authenticator_type" => "1".into(), - "device_identifier" => device_id, - "sms_phone_id" => "1".into(), - }; - - let resp = self - .post(format!( - "{}/ITwoFactorService/AddAuthenticator/v0001", - STEAM_API_BASE.to_string() - )) - .form(¶ms) - .send()?; - self.save_cookies_from_response(&resp); - let text = resp.text()?; - trace!("raw add authenticator response: {}", text); - - let resp: SteamApiResponse = serde_json::from_str(text.as_str())?; - - Ok(resp.response) - } - - /// 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 { - ensure!(matches!(self.session, Some(_))); - let params = hashmap! { - "steamid" => self.session.as_ref().unwrap().expose_secret().steam_id.to_string(), - "access_token" => self.session.as_ref().unwrap().expose_secret().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()?; - - let text = resp.text()?; - trace!("raw finalize authenticator response: {}", text); - - let resp: SteamApiResponse = - serde_json::from_str(text.as_str())?; - - return Ok(resp.response); - } - - /// Host: api.steampowered.com - /// Endpoint: POST /ITwoFactorService/RemoveAuthenticator/v0001 - pub fn remove_authenticator( - &self, - revocation_code: String, - ) -> anyhow::Result { - let params = hashmap! { - "steamid" => self.session.as_ref().unwrap().expose_secret().steam_id.to_string(), - "steamguard_scheme" => "2".into(), - "revocation_code" => revocation_code, - "access_token" => self.session.as_ref().unwrap().expose_secret().token.to_string(), - }; - - let resp = self - .post(format!( - "{}/ITwoFactorService/RemoveAuthenticator/v0001", - STEAM_API_BASE.to_string() - )) - .form(¶ms) - .send()?; - - let text = resp.text()?; - trace!("raw remove authenticator response: {}", text); - - let resp: SteamApiResponse = - serde_json::from_str(text.as_str())?; - - return Ok(resp.response); - } + Ok(resp.into_response_data()) } pub trait BuildableRequest { @@ -521,11 +70,8 @@ impl<'a, T: BuildableRequest> ApiRequest<'a, T> { pub(crate) fn build_url(&self) -> String { format!( - "{}/I{}Service/{}/v{}", - STEAM_API_BASE.to_string(), - self.api_interface, - self.api_method, - self.api_version + "{}/{}/{}/v{}", + *STEAM_API_BASE, self.api_interface, self.api_method, self.api_version ) } @@ -546,11 +92,6 @@ impl ApiResponse { self.result } - pub(crate) fn with_error_message(mut self, error_message: Option) -> Self { - self.error_message = error_message; - self - } - pub fn error_message(&self) -> Option<&String> { self.error_message.as_ref() } diff --git a/steamguard/src/steamapi/authentication.rs b/steamguard/src/steamapi/authentication.rs index b336ba2..0fff3c9 100644 --- a/steamguard/src/steamapi/authentication.rs +++ b/steamguard/src/steamapi/authentication.rs @@ -1,34 +1,14 @@ use crate::{ protobufs::{ custom::CAuthentication_BeginAuthSessionViaCredentials_Request_BinaryGuardData, - steammessages_auth_steamclient::{ - CAuthenticationSupport_RevokeToken_Request, - CAuthenticationSupport_RevokeToken_Response, - CAuthentication_AccessToken_GenerateForApp_Request, - CAuthentication_AccessToken_GenerateForApp_Response, - CAuthentication_BeginAuthSessionViaCredentials_Request, - CAuthentication_BeginAuthSessionViaCredentials_Response, - CAuthentication_BeginAuthSessionViaQR_Request, - CAuthentication_BeginAuthSessionViaQR_Response, - CAuthentication_GetAuthSessionInfo_Request, - CAuthentication_GetPasswordRSAPublicKey_Request, - CAuthentication_GetPasswordRSAPublicKey_Response, - CAuthentication_MigrateMobileSession_Request, - CAuthentication_MigrateMobileSession_Response, - CAuthentication_PollAuthSessionStatus_Request, - CAuthentication_PollAuthSessionStatus_Response, - CAuthentication_RefreshToken_Revoke_Request, - CAuthentication_RefreshToken_Revoke_Response, - CAuthentication_UpdateAuthSessionWithMobileConfirmation_Request, - CAuthentication_UpdateAuthSessionWithMobileConfirmation_Response, - CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request, - CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response, EAuthSessionGuardType, - }, + steammessages_auth_steamclient::*, }, token::Jwt, transport::Transport, }; +const SERVICE_NAME: &str = "IAuthenticationService"; + use super::{ApiRequest, ApiResponse, BuildableRequest}; #[derive(Debug)] @@ -52,12 +32,7 @@ where &mut self, req: CAuthentication_BeginAuthSessionViaCredentials_Request_BinaryGuardData, ) -> anyhow::Result> { - let req = ApiRequest::new( - "Authentication", - "BeginAuthSessionViaCredentials", - 1u32, - req, - ); + let req = ApiRequest::new(SERVICE_NAME, "BeginAuthSessionViaCredentials", 1u32, req); let resp = self.transport.send_request::< CAuthentication_BeginAuthSessionViaCredentials_Request_BinaryGuardData, CAuthentication_BeginAuthSessionViaCredentials_Response>(req)?; @@ -68,7 +43,7 @@ where &mut self, req: CAuthentication_BeginAuthSessionViaQR_Request, ) -> anyhow::Result> { - let req = ApiRequest::new("Authentication", "BeginAuthSessionViaQR", 1u32, req); + let req = ApiRequest::new(SERVICE_NAME, "BeginAuthSessionViaQR", 1u32, req); let resp = self .transport .send_request::( @@ -82,7 +57,7 @@ where req: CAuthentication_AccessToken_GenerateForApp_Request, access_token: &Jwt, ) -> anyhow::Result> { - let req = ApiRequest::new("Authentication", "GenerateAccessTokenForApp", 1u32, req) + let req = ApiRequest::new(SERVICE_NAME, "GenerateAccessTokenForApp", 1u32, req) .with_access_token(access_token); let resp = self .transport @@ -98,21 +73,21 @@ where ) -> anyhow::Result> { let mut inner = CAuthentication_GetPasswordRSAPublicKey_Request::new(); inner.set_account_name(account_name); - let req = ApiRequest::new("Authentication", "GetPasswordRSAPublicKey", 1u32, inner); + let req = ApiRequest::new(SERVICE_NAME, "GetPasswordRSAPublicKey", 1u32, inner); let resp = self .transport .send_request::( req, )?; - return Ok(resp); + Ok(resp) } pub fn migrate_mobile_session( &mut self, req: CAuthentication_MigrateMobileSession_Request, ) -> anyhow::Result> { - let req = ApiRequest::new("Authentication", "MigrateMobileSession", 1u32, req); + let req = ApiRequest::new(SERVICE_NAME, "MigrateMobileSession", 1u32, req); let resp = self .transport .send_request::( @@ -125,7 +100,7 @@ where &mut self, req: CAuthentication_PollAuthSessionStatus_Request, ) -> anyhow::Result> { - let req = ApiRequest::new("Authentication", "PollAuthSessionStatus", 1u32, req); + let req = ApiRequest::new(SERVICE_NAME, "PollAuthSessionStatus", 1u32, req); let resp = self .transport .send_request::( @@ -138,7 +113,7 @@ where &mut self, req: CAuthentication_RefreshToken_Revoke_Request, ) -> anyhow::Result> { - let req = ApiRequest::new("Authentication", "RevokeRefreshToken", 1u32, req); + let req = ApiRequest::new(SERVICE_NAME, "RevokeRefreshToken", 1u32, req); let resp = self .transport .send_request::( @@ -151,7 +126,7 @@ where &mut self, req: CAuthenticationSupport_RevokeToken_Request, ) -> anyhow::Result> { - let req = ApiRequest::new("Authentication", "RevokeToken", 1u32, req); + let req = ApiRequest::new(SERVICE_NAME, "RevokeToken", 1u32, req); let resp = self .transport .send_request::( @@ -166,7 +141,7 @@ where ) -> anyhow::Result> { let req = ApiRequest::new( - "Authentication", + SERVICE_NAME, "UpdateAuthSessionWithMobileConfirmation", 1u32, req, @@ -184,7 +159,7 @@ where req: CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request, ) -> anyhow::Result> { let req = ApiRequest::new( - "Authentication", + SERVICE_NAME, "UpdateAuthSessionWithSteamGuardCode", 1u32, req, diff --git a/steamguard/src/steamapi/twofactor.rs b/steamguard/src/steamapi/twofactor.rs index 74ad6d7..369e389 100644 --- a/steamguard/src/steamapi/twofactor.rs +++ b/steamguard/src/steamapi/twofactor.rs @@ -6,6 +6,8 @@ use super::{ApiRequest, ApiResponse, BuildableRequest}; use crate::protobufs::custom::CTwoFactor_Time_Request; use crate::protobufs::service_twofactor::*; +const SERVICE_NAME: &str = "ITwoFactorService"; + #[derive(Debug)] pub struct TwoFactorClient where @@ -28,7 +30,7 @@ where req: CTwoFactor_AddAuthenticator_Request, access_token: &Jwt, ) -> anyhow::Result> { - let req = ApiRequest::new("TwoFactor", "AddAuthenticator", 1, req) + let req = ApiRequest::new(SERVICE_NAME, "AddAuthenticator", 1, req) .with_access_token(access_token); let resp = self .transport @@ -43,7 +45,7 @@ where req: CTwoFactor_FinalizeAddAuthenticator_Request, access_token: &Jwt, ) -> anyhow::Result> { - let req = ApiRequest::new("TwoFactor", "FinalizeAddAuthenticator", 1, req) + let req = ApiRequest::new(SERVICE_NAME, "FinalizeAddAuthenticator", 1, req) .with_access_token(access_token); let resp = self .transport @@ -58,7 +60,7 @@ where req: CTwoFactor_RemoveAuthenticator_Request, access_token: &Jwt, ) -> anyhow::Result> { - let req = ApiRequest::new("TwoFactor", "RemoveAuthenticator", 1, req) + let req = ApiRequest::new(SERVICE_NAME, "RemoveAuthenticator", 1, req) .with_access_token(access_token); let resp = self .transport @@ -74,7 +76,7 @@ where access_token: &Jwt, ) -> anyhow::Result> { let req = - ApiRequest::new("TwoFactor", "QueryStatus", 1, req).with_access_token(access_token); + ApiRequest::new(SERVICE_NAME, "QueryStatus", 1, req).with_access_token(access_token); let resp = self .transport .send_request::(req)?; @@ -82,7 +84,7 @@ where } pub fn query_time(&mut self) -> anyhow::Result> { - let req = ApiRequest::new("TwoFactor", "QueryTime", 1, CTwoFactor_Time_Request::new()); + let req = ApiRequest::new(SERVICE_NAME, "QueryTime", 1, CTwoFactor_Time_Request::new()); let resp = self .transport .send_request::(req)?; diff --git a/steamguard/src/token.rs b/steamguard/src/token.rs index d17670a..778ca41 100644 --- a/steamguard/src/token.rs +++ b/steamguard/src/token.rs @@ -1,27 +1,30 @@ -use regex::bytes; use secrecy::{ExposeSecret, Secret, SecretString}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryInto; -use zeroize::Zeroize; #[derive(Debug, Clone)] pub struct TwoFactorSecret(Secret<[u8; 20]>); -// pub struct TwoFactorSecret(Secret>); + +impl Default for TwoFactorSecret { + fn default() -> Self { + Self::new() + } +} impl TwoFactorSecret { pub fn new() -> Self { - return Self([0u8; 20].into()); + Self([0u8; 20].into()) } pub fn from_bytes(bytes: Vec) -> Self { let bytes: [u8; 20] = bytes[..].try_into().unwrap(); - return Self(bytes.into()); + Self(bytes.into()) } pub fn parse_shared_secret(secret: String) -> anyhow::Result { - ensure!(secret.len() != 0, "unable to parse empty shared secret"); + ensure!(!secret.is_empty(), "unable to parse empty shared secret"); let result: [u8; 20] = base64::decode(secret)?.try_into().unwrap(); - return Ok(Self(result.into())); + Ok(Self(result.into())) } /// Generate a 5 character 2FA code to that can be used to log in to Steam. @@ -37,17 +40,17 @@ impl TwoFactorSecret { let mut code_array: [u8; 5] = [0; 5]; let b = (hashed_data[19] & 0xF) as usize; let mut code_point: i32 = ((hashed_data[b] & 0x7F) as i32) << 24 - | ((hashed_data[b + 1] & 0xFF) as i32) << 16 - | ((hashed_data[b + 2] & 0xFF) as i32) << 8 - | ((hashed_data[b + 3] & 0xFF) as i32); + | (hashed_data[b + 1] as i32) << 16 + | (hashed_data[b + 2] as i32) << 8 + | (hashed_data[b + 3] as i32); - for i in 0..5 { - code_array[i] = steam_guard_code_translations + for item in &mut code_array { + *item = steam_guard_code_translations [code_point as usize % steam_guard_code_translations.len()]; code_point /= steam_guard_code_translations.len() as i32; } - return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap(); + String::from_utf8(code_array.to_vec()).unwrap() } } @@ -56,7 +59,7 @@ impl Serialize for TwoFactorSecret { where S: Serializer, { - serializer.serialize_str(base64::encode(&self.0.expose_secret()).as_str()) + serializer.serialize_str(base64::encode(self.0.expose_secret()).as_str()) } } @@ -71,14 +74,14 @@ impl<'de> Deserialize<'de> for TwoFactorSecret { impl PartialEq for TwoFactorSecret { fn eq(&self, other: &Self) -> bool { - return self.0.expose_secret() == other.0.expose_secret(); + self.0.expose_secret() == other.0.expose_secret() } } impl Eq for TwoFactorSecret {} fn build_time_bytes(time: u64) -> [u8; 8] { - return time.to_be_bytes(); + time.to_be_bytes() } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -112,7 +115,7 @@ impl Serialize for Jwt { where S: Serializer, { - serializer.serialize_str(&self.0.expose_secret()) + serializer.serialize_str(self.0.expose_secret()) } } @@ -132,8 +135,8 @@ impl From for Jwt { } } -fn decode_jwt(jwt: &String) -> anyhow::Result { - let parts = jwt.split(".").collect::>(); +fn decode_jwt(jwt: impl AsRef) -> anyhow::Result { + let parts = jwt.as_ref().split('.').collect::>(); ensure!(parts.len() == 3, "Invalid JWT"); let data = parts[1]; @@ -164,13 +167,13 @@ impl SteamJwtData { mod tests { use super::*; + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + struct FooBar { + secret: TwoFactorSecret, + } + #[test] fn test_serialize_2fa_secret() -> anyhow::Result<()> { - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - struct FooBar { - secret: TwoFactorSecret, - } - let secret = FooBar { secret: TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?, }; @@ -178,32 +181,21 @@ mod tests { let serialized = serde_json::to_string(&secret)?; assert_eq!(serialized, "{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}"); - return Ok(()); + Ok(()) } #[test] fn test_deserialize_2fa_secret() -> anyhow::Result<()> { - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - struct FooBar { - secret: TwoFactorSecret, - } - - let secret: FooBar = - serde_json::from_str(&"{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}")?; + let secret: FooBar = serde_json::from_str("{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}")?; let code = secret.secret.generate_code(1616374841u64); assert_eq!(code, "2F9J5"); - return Ok(()); + Ok(()) } #[test] fn test_serialize_and_deserialize_2fa_secret() -> anyhow::Result<()> { - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - struct FooBar { - secret: TwoFactorSecret, - } - let secret = FooBar { secret: TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?, }; @@ -212,7 +204,7 @@ mod tests { let deserialized: FooBar = serde_json::from_str(&serialized)?; assert_eq!(deserialized, secret); - return Ok(()); + Ok(()) } #[test] @@ -232,7 +224,7 @@ mod tests { let code = secret.generate_code(1616374841u64); assert_eq!(code, "2F9J5"); - return Ok(()); + Ok(()) } #[test] diff --git a/steamguard/src/transport/mod.rs b/steamguard/src/transport/mod.rs index 11386e3..d587661 100644 --- a/steamguard/src/transport/mod.rs +++ b/steamguard/src/transport/mod.rs @@ -1,7 +1,6 @@ pub mod webapi; use protobuf::MessageFull; -use serde::{Deserialize, Serialize}; pub use webapi::WebApiTransport; use crate::steamapi::{ApiRequest, ApiResponse, BuildableRequest}; diff --git a/steamguard/src/transport/webapi.rs b/steamguard/src/transport/webapi.rs index bc0def1..81c1f2e 100644 --- a/steamguard/src/transport/webapi.rs +++ b/steamguard/src/transport/webapi.rs @@ -15,16 +15,22 @@ pub struct WebApiTransport { client: reqwest::blocking::Client, } +impl Default for WebApiTransport { + fn default() -> Self { + Self::new() + } +} + impl WebApiTransport { pub fn new() -> WebApiTransport { - return WebApiTransport { + Self { client: reqwest::blocking::Client::new(), // client: reqwest::blocking::Client::builder() // .danger_accept_invalid_certs(true) // .proxy(reqwest::Proxy::all("http://localhost:8080").unwrap()) // .build() // .unwrap(), - }; + } } } @@ -95,7 +101,7 @@ impl Transport for WebApiTransport { response_data: res, }; - return Ok(api_resp); + Ok(api_resp) } fn close(&mut self) {} @@ -108,8 +114,7 @@ fn encode_msg(msg: &T, config: base64::Config) -> anyhow::Result } fn decode_msg(bytes: &[u8]) -> anyhow::Result { - // let bytes = base64::decode_config(b64, base64::STANDARD)?; - let msg = T::parse_from_bytes(bytes.as_ref())?; + let msg = T::parse_from_bytes(bytes)?; Ok(msg) } diff --git a/steamguard/src/userlogin.rs b/steamguard/src/userlogin.rs index 0ddcdde..49c1997 100644 --- a/steamguard/src/userlogin.rs +++ b/steamguard/src/userlogin.rs @@ -7,37 +7,22 @@ use crate::protobufs::steammessages_auth_steamclient::{ EAuthSessionGuardType, }; use crate::steamapi::authentication::AuthenticationClient; -use crate::steamapi::{ApiRequest, ApiResponse, EResult}; +use crate::steamapi::EResult; use crate::token::Tokens; -use crate::transport::Transport; use crate::{ - api_responses::{LoginResponse, RsaResponse}, protobufs::steammessages_auth_steamclient::{ - CAuthenticationSupport_RevokeToken_Request, CAuthenticationSupport_RevokeToken_Response, - CAuthentication_AccessToken_GenerateForApp_Request, - CAuthentication_AccessToken_GenerateForApp_Response, - CAuthentication_BeginAuthSessionViaCredentials_Request, CAuthentication_BeginAuthSessionViaCredentials_Response, CAuthentication_BeginAuthSessionViaQR_Request, CAuthentication_BeginAuthSessionViaQR_Response, - CAuthentication_GetPasswordRSAPublicKey_Request, CAuthentication_GetPasswordRSAPublicKey_Response, - CAuthentication_MigrateMobileSession_Request, - CAuthentication_MigrateMobileSession_Response, CAuthentication_RefreshToken_Revoke_Request, - CAuthentication_RefreshToken_Revoke_Response, - CAuthentication_UpdateAuthSessionWithMobileConfirmation_Request, - CAuthentication_UpdateAuthSessionWithMobileConfirmation_Response, CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request, CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response, EAuthTokenPlatformType, }, - steamapi::{Session, SteamApiClient}, transport::WebApiTransport, }; use log::*; use rsa::{PublicKey, RsaPublicKey}; -use secrecy::ExposeSecret; -use serde::{Deserialize, Serialize}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; #[derive(Debug)] pub enum LoginError { @@ -98,7 +83,6 @@ impl BeginQrLoginResponse { /// Handles the user login flow. #[derive(Debug)] pub struct UserLogin { - platform_type: EAuthTokenPlatformType, client: AuthenticationClient, device_details: DeviceDetails, @@ -106,32 +90,31 @@ pub struct UserLogin { } impl UserLogin { - pub fn new(platform_type: EAuthTokenPlatformType, device_details: DeviceDetails) -> Self { - return Self { - platform_type, + pub fn new(device_details: DeviceDetails) -> Self { + Self { client: AuthenticationClient::new(WebApiTransport::new()), device_details, started_auth: None, - }; + } } pub fn begin_auth_via_credentials( &mut self, - account_name: &String, - password: &String, + account_name: &str, + password: &str, ) -> anyhow::Result, LoginError> { if self.started_auth.is_some() { return Err(LoginError::AuthAlreadyStarted); } trace!("UserLogin::begin_auth_via_credentials"); - let rsa = self.client.fetch_rsa_key(account_name.clone())?; + let rsa = self.client.fetch_rsa_key(account_name.to_owned())?; let mut req = CAuthentication_BeginAuthSessionViaCredentials_Request_BinaryGuardData::new(); - req.set_account_name(account_name.clone()); + req.set_account_name(account_name.to_owned()); let rsa_resp = rsa.into_response_data(); req.set_encryption_timestamp(rsa_resp.timestamp()); - let encrypted_password = encrypt_password(rsa_resp, &password); + let encrypted_password = encrypt_password(rsa_resp, password); req.set_encrypted_password(encrypted_password); req.set_persistence(ESessionPersistence::k_ESessionPersistence_Persistent); req.device_details = self.device_details.clone().into_message_field(); @@ -280,7 +263,7 @@ impl UserLogin { fn encrypt_password( rsa_resp: CAuthentication_GetPasswordRSAPublicKey_Response, - password: &String, + password: impl AsRef<[u8]>, ) -> String { let rsa_exponent = rsa::BigUint::parse_bytes(rsa_resp.publickey_exp().as_bytes(), 16).unwrap(); let rsa_modulus = rsa::BigUint::parse_bytes(rsa_resp.publickey_mod().as_bytes(), 16).unwrap(); @@ -290,12 +273,11 @@ fn encrypt_password( #[cfg(not(test))] let mut rng = rand::rngs::OsRng; let padding = rsa::PaddingScheme::new_pkcs1v15_encrypt(); - let encrypted_password = base64::encode( + base64::encode( public_key - .encrypt(&mut rng, padding, password.as_bytes()) + .encrypt(&mut rng, padding, password.as_ref()) .unwrap(), - ); - return encrypted_password; + ) } #[derive(Debug)] @@ -427,7 +409,7 @@ mod tests { rsa_resp.set_publickey_exp(String::from("010001")); rsa_resp.set_publickey_mod(String::from("98f9088c1250b17fe19d2b2422d54a1eef0036875301731f11bd17900e215318eb6de1546727c0b7b61b86cefccdcb2f8108c813154d9a7d55631965eece810d4ab9d8a59c486bda778651b876176070598a93c2325c275cb9c17bdbcacf8edc9c18c0c5d59bc35703505ef8a09ed4c62b9f92a3fac5740ce25e490ab0e26d872140e4103d912d1e3958f844264211277ee08d2b4dd3ac58b030b25342bd5c949ae7794e46a8eab26d5a8deca683bfd381da6c305b19868b8c7cd321ce72c693310a6ebf2ecd43642518f825894602f6c239cf193cb4346ce64beac31e20ef88f934f2f776597734bb9eae1ebdf4a453973b6df9d5e90777bffe5db83dd1757b")); rsa_resp.set_timestamp(1); - let result = encrypt_password(rsa_resp, &String::from("kelwleofpsm3n4ofc")); + let result = encrypt_password(rsa_resp, "kelwleofpsm3n4ofc"); assert_eq!(result.len(), 344); assert_eq!(result, "RUo/3IfbkVcJi1q1S5QlpKn1mEn3gNJoc/Z4VwxRV9DImV6veq/YISEuSrHB3885U5MYFLn1g94Y+cWRL6HGXoV+gOaVZe43m7O92RwiVz6OZQXMfAv3UC/jcqn/xkitnj+tNtmx55gCxmGbO2KbqQ0TQqAyqCOOw565B+Cwr2OOorpMZAViv9sKA/G3Q6yzscU6rhua179c8QjC1Hk3idUoSzpWfT4sHNBW/EREXZ3Dkjwu17xzpfwIUpnBVIlR8Vj3coHgUCpTsKVRA3T814v9BYPlvLYwmw5DW3ddx+2SyTY0P5uuog36TN2PqYS7ioF5eDe16gyfRR4Nzn/7wA=="); } @@ -438,7 +420,7 @@ mod tests { rsa_resp.set_publickey_exp(String::from("010001")); rsa_resp.set_publickey_mod(String::from("ca6a8dc290279b25c38a282b9a7b01306c5978bd7a2f60dcfd52134ac58faf121568ebd85ca6a2128413b76ec70fb3150b3181bbe2a1a8349b68da9c303960bdf4e34296b27bd4ea29b4d1a695168ddfc974bb6ba427206fdcdb088bf27261a52f343a51e19759fe4072b7a2047a6bc31361950d9e87d7977b31b71696572babe45ea6a7d132547984462fd5787607e0d9ff1c637e04d593e7538c880c3cdd252b75bcb703a7b8bb01cd8898b04980f40b76235d50fc1544c39ccbe763892322fc6d0a5acaf8be09efbc20fcfebcd3b02a1eb95d9d0c338e96674c17edbb0257cd43d04974423f1f995a28b9e159322d9db2708826804c0eccafffc94dd2a3d5")); rsa_resp.set_timestamp(104444850000); - let result = encrypt_password(rsa_resp, &String::from("foo")); + let result = encrypt_password(rsa_resp, "foo"); assert_eq!(result, "jmlMXmhbweWn+wJnnf96W3Lsh0dRmzrBfMxREUuEW11rRYcfXWupBIT3eK1fmQHMZmyJeMhZiRpgIaZ7DafojQT6djJr+RKeREJs0ys9hKwxD5FGlqsTLXXEeuyopyd2smHBbmmF47voe59KEoiZZapP+eYnpJy3O2k7e1P9BH9LsKIN/nWF1ogM2jjJ328AejUpM64tPl/kInFJ1CHrLiAAKDPk42fLAAKs97xIi0JkosG6yp+8HhFqQxxZ8/bNI1IVkQC1Hdc2AN0QlNKxbDXquAn6ARgw/4b5DwUpnOb9de+Q6iX3v1/M07Se7JV8/4tuz8Thy2Chbxsf9E1TuQ=="); } }