From 610cda120e002f23bbb186cca6209e797dd94799 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 20 Jun 2022 20:05:00 -0400 Subject: [PATCH 1/3] get_server_time now returns Result, fixes #152 --- src/main.rs | 4 +-- steamguard/src/accountlinker.rs | 2 +- steamguard/src/lib.rs | 18 ++++++------ steamguard/src/steamapi.rs | 50 ++++++++++++++++++++++++++++----- steamguard/src/token.rs | 12 ++++---- 5 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1cb0ff0..34981a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -261,7 +261,7 @@ fn do_login_impl( } Err(LoginError::Need2FA) => match account { Some(a) => { - let server_time = steamapi::get_server_time(); + let server_time = steamapi::get_server_time()?.server_time; login.twofactor_code = a.generate_code(server_time); } None => { @@ -663,7 +663,7 @@ fn do_subcmd_decrypt( } fn do_subcmd_code(selected_accounts: Vec>>) -> anyhow::Result<()> { - let server_time = steamapi::get_server_time(); + let server_time = steamapi::get_server_time()?.server_time; debug!("Time used to generate codes: {}", server_time); for account in selected_accounts { info!( diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 9f1693e..a7249fe 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -82,7 +82,7 @@ impl AccountLinker { account: &mut SteamGuardAccount, sms_code: String, ) -> anyhow::Result<(), FinalizeLinkError> { - let time = crate::steamapi::get_server_time(); + let time = crate::steamapi::get_server_time()?.server_time; let code = account.generate_code(time); let resp: FinalizeAddAuthenticatorResponse = self.client diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index f380053..9d44a0c 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -62,11 +62,11 @@ pub struct SteamGuardAccount { pub session: Option>, } -fn build_time_bytes(time: i64) -> [u8; 8] { +fn build_time_bytes(time: u64) -> [u8; 8] { return time.to_be_bytes(); } -fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String { +fn generate_confirmation_hash_for_time(time: u64, tag: &str, identity_secret: &String) -> String { let decode: &[u8] = &base64::decode(&identity_secret).unwrap(); let time_bytes = build_time_bytes(time); let tag_bytes = tag.as_bytes(); @@ -98,13 +98,12 @@ impl SteamGuardAccount { self.session = Some(session.into()); } - pub fn generate_code(&self, time: i64) -> String { + pub fn generate_code(&self, time: u64) -> String { return self.shared_secret.generate_code(time); } - fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> { + fn get_confirmation_query_params(&self, tag: &str, time: u64) -> HashMap<&str, String> { let session = self.session.as_ref().unwrap().expose_secret(); - let time = steamapi::get_server_time(); let mut params = HashMap::new(); params.insert("p", self.device_id.clone()); params.insert("a", session.steam_id.to_string()); @@ -145,12 +144,13 @@ impl SteamGuardAccount { .cookie_store(true) .build()?; + let time = steamapi::get_server_time()?.server_time; let resp = client .get("https://steamcommunity.com/mobileconf/conf".parse::().unwrap()) .header("X-Requested-With", "com.valvesoftware.android.steam.community") .header(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") .header(COOKIE, cookies.cookies(&url).unwrap()) - .query(&self.get_confirmation_query_params("conf")) + .query(&self.get_confirmation_query_params("conf", time)) .send()?; trace!("{:?}", resp); @@ -173,7 +173,8 @@ impl SteamGuardAccount { .cookie_store(true) .build()?; - let mut query_params = self.get_confirmation_query_params("conf"); + 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()); query_params.insert("ck", conf.key.to_string()); @@ -217,7 +218,8 @@ impl SteamGuardAccount { .cookie_store(true) .build()?; - let query_params = self.get_confirmation_query_params("details"); + let time = steamapi::get_server_time()?.server_time; + let query_params = self.get_confirmation_query_params("details", time); let resp: ConfirmationDetailsResponse = client.get(format!("https://steamcommunity.com/mobileconf/details/{}", conf.id).parse::().unwrap()) .header("X-Requested-With", "com.valvesoftware.android.steam.community") diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 7ddebd7..a779f41 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -113,17 +113,53 @@ impl SerializableSecret for Session {} impl CloneableSecret for Session {} impl DebugSecret for Session {} -pub fn get_server_time() -> i64 { +/// Represents the response from `/ITwoFactorService/QueryTime/v0001` +#[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, +} + +/// Queries Steam for the current time. +/// +/// Endpoint: `/ITwoFactorService/QueryTime/v0001` +/// +/// Example Response: +/// ```json +/// { +/// "response": { +/// "server_time": "1655768666", +/// "skew_tolerance_seconds": "60", +/// "large_time_jink": "86400", +/// "probe_frequency_seconds": 3600, +/// "adjusted_time_probe_frequency_seconds": 300, +/// "hint_probe_frequency_seconds": 60, +/// "sync_timeout": 60, +/// "try_again_seconds": 900, +/// "max_attempts": 3 +/// } +/// } +/// ``` +pub fn get_server_time() -> anyhow::Result { let client = reqwest::blocking::Client::new(); let resp = client .post("https://api.steampowered.com/ITwoFactorService/QueryTime/v0001") .body("steamid=0") - .send(); - let value: serde_json::Value = resp.unwrap().json().unwrap(); + .send()?; + let resp: SteamApiResponse = resp.json()?; - return String::from(value["response"]["server_time"].as_str().unwrap()) - .parse() - .unwrap(); + 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. @@ -457,7 +493,7 @@ impl SteamApiClient { &self, sms_code: String, code_2fa: String, - time_2fa: i64, + time_2fa: u64, ) -> anyhow::Result { ensure!(matches!(self.session, Some(_))); let params = hashmap! { diff --git a/steamguard/src/token.rs b/steamguard/src/token.rs index 30cd319..434e149 100644 --- a/steamguard/src/token.rs +++ b/steamguard/src/token.rs @@ -18,14 +18,14 @@ impl TwoFactorSecret { } /// Generate a 5 character 2FA code to that can be used to log in to Steam. - pub fn generate_code(&self, time: i64) -> String { + pub fn generate_code(&self, time: u64) -> String { let steam_guard_code_translations: [u8; 26] = [ 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89, ]; // this effectively makes it so that it creates a new code every 30 seconds. - let time_bytes: [u8; 8] = build_time_bytes(time / 30i64); + let time_bytes: [u8; 8] = build_time_bytes(time / 30u64); let hashed_data = hmacsha1::hmac_sha1(self.0.expose_secret(), &time_bytes); let mut code_array: [u8; 5] = [0; 5]; let b = (hashed_data[19] & 0xF) as usize; @@ -70,7 +70,7 @@ impl PartialEq for TwoFactorSecret { impl Eq for TwoFactorSecret {} -fn build_time_bytes(time: i64) -> [u8; 8] { +fn build_time_bytes(time: u64) -> [u8; 8] { return time.to_be_bytes(); } @@ -104,7 +104,7 @@ mod tests { let secret: FooBar = serde_json::from_str(&"{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}")?; - let code = secret.secret.generate_code(1616374841i64); + let code = secret.secret.generate_code(1616374841u64); assert_eq!(code, "2F9J5"); return Ok(()); @@ -130,7 +130,7 @@ mod tests { #[test] fn test_build_time_bytes() { - let t1 = build_time_bytes(1617591917i64); + let t1 = build_time_bytes(1617591917u64); let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109]; assert!( t1.iter().zip(t2.iter()).all(|(a, b)| a == b), @@ -143,7 +143,7 @@ mod tests { fn test_generate_code() -> anyhow::Result<()> { let secret = TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?; - let code = secret.generate_code(1616374841i64); + let code = secret.generate_code(1616374841u64); assert_eq!(code, "2F9J5"); return Ok(()); } From fdc61e63d111fe6703116f2a87063e1468222751 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 20 Jun 2022 20:43:53 -0400 Subject: [PATCH 2/3] add --offline flag for generating codes, closes #155 --- src/cli.rs | 26 ++++++++++++++++++++++++++ src/main.rs | 14 +++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 687d904..2931fd7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,6 +32,12 @@ pub(crate) struct Args { #[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( + long, + help = "Assume the computer's time is correct. Don't ask Steam for the time when generating codes." + )] + pub offline: bool, + #[clap(subcommand)] pub sub: Option, } @@ -143,3 +149,23 @@ pub(crate) struct ArgsEncrypt; #[derive(Debug, Clone, Parser)] #[clap(about = "Decrypt all maFiles")] pub(crate) struct ArgsDecrypt; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Generate 2FA codes")] +pub(crate) struct ArgsCode { + #[clap( + long, + help = "Assume the computer's time is correct. Don't ask Steam for the time when generating codes." + )] + pub offline: bool, +} + +// HACK: the derive API doesn't support default subcommands, so we are going to make it so that it'll be easier to switch over when it's implemented. +// See: https://github.com/clap-rs/clap/issues/3857 +impl From for ArgsCode { + fn from(args: Args) -> Self { + ArgsCode { + offline: args.offline, + } + } +} diff --git a/src/main.rs b/src/main.rs index 34981a6..80cdf47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ extern crate rpassword; use clap::{IntoApp, Parser}; use log::*; +use std::time::{SystemTime, UNIX_EPOCH}; use std::{ io::{stdout, Write}, path::Path, @@ -180,7 +181,7 @@ fn run() -> anyhow::Result<()> { } _ => { debug!("No subcommand given, assuming user wants a 2fa code"); - return do_subcmd_code(selected_accounts); + return do_subcmd_code(args.into(), selected_accounts); } } } @@ -662,8 +663,15 @@ fn do_subcmd_decrypt( return Ok(()); } -fn do_subcmd_code(selected_accounts: Vec>>) -> anyhow::Result<()> { - let server_time = steamapi::get_server_time()?.server_time; +fn do_subcmd_code( + args: cli::ArgsCode, + selected_accounts: Vec>>, +) -> anyhow::Result<()> { + let server_time = if args.offline { + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + } else { + steamapi::get_server_time()?.server_time + }; debug!("Time used to generate codes: {}", server_time); for account in selected_accounts { info!( From a52af3675b52d3994fface9cc18f5f4b378f7ff6 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 21 Jun 2022 19:31:17 -0400 Subject: [PATCH 3/3] add code subcommand back in, because it makes the arg parsing better --- src/cli.rs | 14 +++++--------- src/main.rs | 15 +++++++-------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 2931fd7..947f915 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,14 +32,11 @@ pub(crate) struct Args { #[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( - long, - help = "Assume the computer's time is correct. Don't ask Steam for the time when generating codes." - )] - pub offline: bool, - #[clap(subcommand)] pub sub: Option, + + #[clap(flatten)] + pub code: ArgsCode, } #[derive(Debug, Clone, Parser)] @@ -52,6 +49,7 @@ pub(crate) enum Subcommands { Remove(ArgsRemove), Encrypt(ArgsEncrypt), Decrypt(ArgsDecrypt), + Code(ArgsCode), } #[derive(Debug, Clone, Copy, ArgEnum)] @@ -164,8 +162,6 @@ pub(crate) struct ArgsCode { // See: https://github.com/clap-rs/clap/issues/3857 impl From for ArgsCode { fn from(args: Args) -> Self { - ArgsCode { - offline: args.offline, - } + args.code } } diff --git a/src/main.rs b/src/main.rs index 80cdf47..60c7d89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -168,21 +168,20 @@ fn run() -> anyhow::Result<()> { .collect::>() ); - match args.sub { - Some(cli::Subcommands::Trade(args)) => { + match args.sub.unwrap_or(cli::Subcommands::Code(args.code)) { + cli::Subcommands::Trade(args) => { return do_subcmd_trade(args, &mut manifest, selected_accounts); } - Some(cli::Subcommands::Remove(args)) => { + cli::Subcommands::Remove(args) => { return do_subcmd_remove(args, &mut manifest, selected_accounts); } - Some(s) => { + cli::Subcommands::Code(args) => { + return do_subcmd_code(args, selected_accounts); + } + s => { error!("Unknown subcommand: {:?}", s); return Err(errors::UserError::UnknownSubcommand.into()); } - _ => { - debug!("No subcommand given, assuming user wants a 2fa code"); - return do_subcmd_code(args.into(), selected_accounts); - } } }