From 610cda120e002f23bbb186cca6209e797dd94799 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 20 Jun 2022 20:05:00 -0400 Subject: [PATCH] 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(()); }