Dead code cleanup, subcommand refactor (#206)
- clean up dead code - fix lints - move Session type to legacy module - refactor service names into constants - refactor build_url to be less restrictive for service names - refactor most commands into their own modules
This commit is contained in:
parent
8defefb11e
commit
cbc46ad8eb
39 changed files with 1027 additions and 1768 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2096,6 +2096,7 @@ dependencies = [
|
|||
"text_io",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<SdaAccount> 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<u8> = vec![];
|
||||
reader.read_to_end(&mut ciphertext)?;
|
||||
|
@ -86,14 +88,12 @@ impl EntryLoader<SdaAccount> 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<secrecy::Secret<steamguard::steamapi::Session>>,
|
||||
pub session: Option<secrecy::Secret<Session>>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[serde(rename = "OAuthToken")]
|
||||
pub token: String,
|
||||
#[serde(rename = "SteamID")]
|
||||
pub steam_id: u64,
|
||||
}
|
||||
|
||||
impl CloneableSecret for Session {}
|
||||
impl DebugSecret for Session {}
|
||||
|
||||
impl From<SdaAccount> for SteamGuardAccount {
|
||||
fn from(value: SdaAccount) -> Self {
|
||||
let steam_id = value
|
||||
|
|
|
@ -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<Vec<MigratingAccount>> {
|
||||
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<MigratingManifest, serde_json::E
|
|||
Ok(MigratingManifest::ManifestV1(manifest))
|
||||
} else if json["version"] == serde_json::Value::Null {
|
||||
let manifest: SdaManifest = serde_json::from_str(&text)?;
|
||||
Ok(MigratingManifest::SDA(manifest))
|
||||
Ok(MigratingManifest::Sda(manifest))
|
||||
} else {
|
||||
Err(serde_json::Error::custom(format!(
|
||||
"Unknown manifest version: {}",
|
||||
|
@ -225,10 +215,7 @@ impl MigratingAccount {
|
|||
}
|
||||
|
||||
pub fn is_latest(&self) -> bool {
|
||||
match self {
|
||||
Self::ManifestV1(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, Self::ManifestV1(_))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
193
src/cli.rs
193
src/cli.rs
|
@ -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<String>,
|
||||
#[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<String>,
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
env = "STEAMGUARD_CLI_PASSKEY",
|
||||
help = "Specify your encryption passkey."
|
||||
)]
|
||||
pub passkey: Option<String>,
|
||||
#[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<Subcommands>,
|
||||
|
||||
#[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<Self, Self::Err> {
|
||||
match s {
|
||||
"error" => Ok(Verbosity::Error),
|
||||
"warn" => Ok(Verbosity::Warn),
|
||||
"info" => Ok(Verbosity::Info),
|
||||
"debug" => Ok(Verbosity::Debug),
|
||||
"trace" => Ok(Verbosity::Trace),
|
||||
_ => Err(anyhow!("Invalid verbosity level: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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<Args> 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,
|
||||
}
|
167
src/commands.rs
Normal file
167
src/commands.rs
Normal file
|
@ -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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
pub(crate) enum CommandType {
|
||||
Const(Box<dyn ConstCommand>),
|
||||
Manifest(Box<dyn ManifestCommand>),
|
||||
Account(Box<dyn AccountCommand>),
|
||||
}
|
||||
|
||||
#[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<Subcommands>,
|
||||
|
||||
#[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<String>,
|
||||
#[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<String>,
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
env = "STEAMGUARD_CLI_PASSKEY",
|
||||
help = "Specify your encryption passkey."
|
||||
)]
|
||||
pub passkey: Option<String>,
|
||||
#[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<Self, Self::Err> {
|
||||
match s {
|
||||
"error" => Ok(Verbosity::Error),
|
||||
"warn" => Ok(Verbosity::Warn),
|
||||
"info" => Ok(Verbosity::Info),
|
||||
"debug" => Ok(Verbosity::Debug),
|
||||
"trace" => Ok(Verbosity::Trace),
|
||||
_ => Err(anyhow!("Invalid verbosity level: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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<Args> for CodeCommand {
|
||||
fn from(args: Args) -> Self {
|
||||
args.code
|
||||
}
|
||||
}
|
45
src/commands/code.rs
Normal file
45
src/commands/code.rs
Normal file
|
@ -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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
18
src/commands/completions.rs
Normal file
18
src/commands/completions.rs
Normal file
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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();
|
43
src/commands/decrypt.rs
Normal file
43
src/commands/decrypt.rs
Normal file
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
src/commands/encrypt.rs
Normal file
39
src/commands/encrypt.rs
Normal file
|
@ -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(())
|
||||
}
|
||||
}
|
42
src/commands/import.rs
Normal file
42
src/commands/import.rs
Normal file
|
@ -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<String>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
55
src/commands/qr.rs
Normal file
55
src/commands/qr.rs
Normal file
|
@ -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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> 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::<unicode::Dense1x2>()
|
||||
.dark_color(unicode::Dense1x2::Light)
|
||||
.light_color(unicode::Dense1x2::Dark)
|
||||
.build()
|
||||
};
|
||||
|
||||
println!("{}", qr_string);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
80
src/commands/remove.rs
Normal file
80
src/commands/remove.rs
Normal file
|
@ -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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
eprintln!(
|
||||
"This will remove the mobile authenticator from {} accounts: {}",
|
||||
accounts.len(),
|
||||
accounts
|
||||
.iter()
|
||||
.map(|a| a.lock().unwrap().account_name.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.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(())
|
||||
}
|
||||
}
|
137
src/commands/setup.rs
Normal file
137
src/commands/setup.rs
Normal file
|
@ -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(())
|
||||
}
|
||||
}
|
115
src/commands/trade.rs
Normal file
115
src/commands/trade.rs
Normal file
|
@ -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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> 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<Confirmation>;
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -45,12 +45,12 @@ impl Default for EncryptionScheme {
|
|||
|
||||
pub trait EntryEncryptor {
|
||||
fn encrypt(
|
||||
passkey: &String,
|
||||
passkey: &str,
|
||||
params: &EntryEncryptionParams,
|
||||
plaintext: Vec<u8>,
|
||||
) -> anyhow::Result<Vec<u8>, EntryEncryptionError>;
|
||||
fn decrypt(
|
||||
passkey: &String,
|
||||
passkey: &str,
|
||||
params: &EntryEncryptionParams,
|
||||
ciphertext: Vec<u8>,
|
||||
) -> anyhow::Result<Vec<u8>, 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<u8>,
|
||||
) -> anyhow::Result<Vec<u8>, 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<u8>,
|
||||
) -> anyhow::Result<Vec<u8>, 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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
574
src/main.rs
574
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<Arc<Mutex<SteamGuardAccount>>>;
|
||||
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::<Vec<String>>()
|
||||
);
|
||||
|
||||
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<Vec<Arc<Mutex<SteamGuardAccount>>>, ManifestAccountLoadError> {
|
||||
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
||||
|
@ -274,13 +254,10 @@ fn do_login_impl(
|
|||
password: String,
|
||||
account: Option<&SteamGuardAccount>,
|
||||
) -> anyhow::Result<Tokens> {
|
||||
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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> 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<Confirmation>;
|
||||
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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> 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::<Vec<String>>()
|
||||
.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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> 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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> 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::<unicode::Dense1x2>()
|
||||
.dark_color(unicode::Dense1x2::Light)
|
||||
.light_color(unicode::Dense1x2::Dark)
|
||||
.build()
|
||||
};
|
||||
|
||||
println!("{}", qr_string);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -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<S>(secret_string: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<secrecy::SecretString, D::Error>
|
||||
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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<Arc<Mutex<SteamGuardAccount>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
for account in selected_accounts {
|
||||
let mut account = account.lock().unwrap();
|
||||
do_login(&mut account)?;
|
||||
info!("Logged in successfully!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
47
src/tui.rs
47
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()?;
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[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::<SteamApiResponse<AddAuthenticatorResponse>>(
|
||||
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::<SteamApiResponse<AddAuthenticatorResponse>>(
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<OAuthData>,
|
||||
pub transfer_urls: Option<Vec<String>>,
|
||||
pub transfer_parameters: Option<LoginTransferParameters>,
|
||||
}
|
||||
|
||||
/// 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<Option<OAuthData>, 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::<LoginResponse>(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::<LoginResponse>(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, "");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<u64, D::Error>
|
||||
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<T> {
|
||||
pub response: T,
|
||||
}
|
||||
|
|
|
@ -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\"}"}
|
|
@ -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\"}"}
|
|
@ -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<T>(r: T) -> anyhow::Result<Self>
|
||||
|
@ -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<Vec<Confirmation>, 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::<Url>().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::<Url>().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=")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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<S>(secret_string: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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::<Url>().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<String>,
|
||||
#[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<QueryTimeResponse> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let resp = client
|
||||
.post("https://api.steampowered.com/ITwoFactorService/QueryTime/v0001")
|
||||
.body("steamid=0")
|
||||
.send()?;
|
||||
let resp: SteamApiResponse<QueryTimeResponse> = 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<secrecy::Secret<Session>>,
|
||||
}
|
||||
|
||||
impl SteamApiClient {
|
||||
pub fn new(session: Option<secrecy::Secret<Session>>) -> 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<CTwoFactor_Time_Response> {
|
||||
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<String> {
|
||||
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<U: reqwest::IntoUrl + std::fmt::Display>(
|
||||
&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<U: reqwest::IntoUrl + std::fmt::Display>(&self, url: U) -> RequestBuilder {
|
||||
self.request(reqwest::Method::GET, url)
|
||||
}
|
||||
|
||||
pub fn post<U: reqwest::IntoUrl + std::fmt::Display>(&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::<Url>().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<LoginResponse> {
|
||||
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<OAuthData> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
return self.phoneajax("has_phone", "null");
|
||||
}
|
||||
|
||||
pub fn check_sms_code(&self, sms_code: String) -> anyhow::Result<bool> {
|
||||
return self.phoneajax("check_sms_code", sms_code.as_str());
|
||||
}
|
||||
|
||||
pub fn check_email_confirmation(&self) -> anyhow::Result<bool> {
|
||||
return self.phoneajax("email_confirmation", "");
|
||||
}
|
||||
|
||||
pub fn add_phone_number(&self, phone_number: String) -> anyhow::Result<bool> {
|
||||
// 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<PhoneValidateResponse> {
|
||||
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::<PhoneValidateResponse>()?;
|
||||
|
||||
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<AddAuthenticatorResponse> {
|
||||
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<AddAuthenticatorResponse> = 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<FinalizeAddAuthenticatorResponse> {
|
||||
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<FinalizeAddAuthenticatorResponse> =
|
||||
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<RemoveAuthenticatorResponse> {
|
||||
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<RemoveAuthenticatorResponse> =
|
||||
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<T> ApiResponse<T> {
|
|||
self.result
|
||||
}
|
||||
|
||||
pub(crate) fn with_error_message(mut self, error_message: Option<String>) -> Self {
|
||||
self.error_message = error_message;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn error_message(&self) -> Option<&String> {
|
||||
self.error_message.as_ref()
|
||||
}
|
||||
|
|
|
@ -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<ApiResponse<CAuthentication_BeginAuthSessionViaCredentials_Response>> {
|
||||
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<ApiResponse<CAuthentication_BeginAuthSessionViaQR_Response>> {
|
||||
let req = ApiRequest::new("Authentication", "BeginAuthSessionViaQR", 1u32, req);
|
||||
let req = ApiRequest::new(SERVICE_NAME, "BeginAuthSessionViaQR", 1u32, req);
|
||||
let resp = self
|
||||
.transport
|
||||
.send_request::<CAuthentication_BeginAuthSessionViaQR_Request, CAuthentication_BeginAuthSessionViaQR_Response>(
|
||||
|
@ -82,7 +57,7 @@ where
|
|||
req: CAuthentication_AccessToken_GenerateForApp_Request,
|
||||
access_token: &Jwt,
|
||||
) -> anyhow::Result<ApiResponse<CAuthentication_AccessToken_GenerateForApp_Response>> {
|
||||
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<ApiResponse<CAuthentication_GetPasswordRSAPublicKey_Response>> {
|
||||
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::<CAuthentication_GetPasswordRSAPublicKey_Request, CAuthentication_GetPasswordRSAPublicKey_Response>(
|
||||
req,
|
||||
)?;
|
||||
|
||||
return Ok(resp);
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub fn migrate_mobile_session(
|
||||
&mut self,
|
||||
req: CAuthentication_MigrateMobileSession_Request,
|
||||
) -> anyhow::Result<ApiResponse<CAuthentication_MigrateMobileSession_Response>> {
|
||||
let req = ApiRequest::new("Authentication", "MigrateMobileSession", 1u32, req);
|
||||
let req = ApiRequest::new(SERVICE_NAME, "MigrateMobileSession", 1u32, req);
|
||||
let resp = self
|
||||
.transport
|
||||
.send_request::<CAuthentication_MigrateMobileSession_Request, CAuthentication_MigrateMobileSession_Response>(
|
||||
|
@ -125,7 +100,7 @@ where
|
|||
&mut self,
|
||||
req: CAuthentication_PollAuthSessionStatus_Request,
|
||||
) -> anyhow::Result<ApiResponse<CAuthentication_PollAuthSessionStatus_Response>> {
|
||||
let req = ApiRequest::new("Authentication", "PollAuthSessionStatus", 1u32, req);
|
||||
let req = ApiRequest::new(SERVICE_NAME, "PollAuthSessionStatus", 1u32, req);
|
||||
let resp = self
|
||||
.transport
|
||||
.send_request::<CAuthentication_PollAuthSessionStatus_Request, CAuthentication_PollAuthSessionStatus_Response>(
|
||||
|
@ -138,7 +113,7 @@ where
|
|||
&mut self,
|
||||
req: CAuthentication_RefreshToken_Revoke_Request,
|
||||
) -> anyhow::Result<ApiResponse<CAuthentication_RefreshToken_Revoke_Response>> {
|
||||
let req = ApiRequest::new("Authentication", "RevokeRefreshToken", 1u32, req);
|
||||
let req = ApiRequest::new(SERVICE_NAME, "RevokeRefreshToken", 1u32, req);
|
||||
let resp = self
|
||||
.transport
|
||||
.send_request::<CAuthentication_RefreshToken_Revoke_Request, CAuthentication_RefreshToken_Revoke_Response>(
|
||||
|
@ -151,7 +126,7 @@ where
|
|||
&mut self,
|
||||
req: CAuthenticationSupport_RevokeToken_Request,
|
||||
) -> anyhow::Result<ApiResponse<CAuthenticationSupport_RevokeToken_Response>> {
|
||||
let req = ApiRequest::new("Authentication", "RevokeToken", 1u32, req);
|
||||
let req = ApiRequest::new(SERVICE_NAME, "RevokeToken", 1u32, req);
|
||||
let resp = self
|
||||
.transport
|
||||
.send_request::<CAuthenticationSupport_RevokeToken_Request, CAuthenticationSupport_RevokeToken_Response>(
|
||||
|
@ -166,7 +141,7 @@ where
|
|||
) -> anyhow::Result<ApiResponse<CAuthentication_UpdateAuthSessionWithMobileConfirmation_Response>>
|
||||
{
|
||||
let req = ApiRequest::new(
|
||||
"Authentication",
|
||||
SERVICE_NAME,
|
||||
"UpdateAuthSessionWithMobileConfirmation",
|
||||
1u32,
|
||||
req,
|
||||
|
@ -184,7 +159,7 @@ where
|
|||
req: CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request,
|
||||
) -> anyhow::Result<ApiResponse<CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response>> {
|
||||
let req = ApiRequest::new(
|
||||
"Authentication",
|
||||
SERVICE_NAME,
|
||||
"UpdateAuthSessionWithSteamGuardCode",
|
||||
1u32,
|
||||
req,
|
||||
|
|
|
@ -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<T>
|
||||
where
|
||||
|
@ -28,7 +30,7 @@ where
|
|||
req: CTwoFactor_AddAuthenticator_Request,
|
||||
access_token: &Jwt,
|
||||
) -> anyhow::Result<ApiResponse<CTwoFactor_AddAuthenticator_Response>> {
|
||||
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<ApiResponse<CTwoFactor_FinalizeAddAuthenticator_Response>> {
|
||||
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<ApiResponse<CTwoFactor_RemoveAuthenticator_Response>> {
|
||||
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<ApiResponse<CTwoFactor_Status_Response>> {
|
||||
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::<CTwoFactor_Status_Request, CTwoFactor_Status_Response>(req)?;
|
||||
|
@ -82,7 +84,7 @@ where
|
|||
}
|
||||
|
||||
pub fn query_time(&mut self) -> anyhow::Result<ApiResponse<CTwoFactor_Time_Response>> {
|
||||
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::<CTwoFactor_Time_Request, CTwoFactor_Time_Response>(req)?;
|
||||
|
|
|
@ -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<Vec<u8>>);
|
||||
|
||||
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<u8>) -> 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<Self> {
|
||||
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<String> for Jwt {
|
|||
}
|
||||
}
|
||||
|
||||
fn decode_jwt(jwt: &String) -> anyhow::Result<SteamJwtData> {
|
||||
let parts = jwt.split(".").collect::<Vec<&str>>();
|
||||
fn decode_jwt(jwt: impl AsRef<str>) -> anyhow::Result<SteamJwtData> {
|
||||
let parts = jwt.as_ref().split('.').collect::<Vec<&str>>();
|
||||
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]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
pub mod webapi;
|
||||
|
||||
use protobuf::MessageFull;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use webapi::WebApiTransport;
|
||||
|
||||
use crate::steamapi::{ApiRequest, ApiResponse, BuildableRequest};
|
||||
|
|
|
@ -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<T: MessageFull>(msg: &T, config: base64::Config) -> anyhow::Result
|
|||
}
|
||||
|
||||
fn decode_msg<T: MessageFull>(bytes: &[u8]) -> anyhow::Result<T> {
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<WebApiTransport>,
|
||||
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<Vec<AllowedConfirmation>, 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==");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue