From cc48bc9537b59c0afc097c8664c915a48642c83e Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 10:39:48 -0400 Subject: [PATCH 1/9] clean up and improve encrypt_password unit test --- src/steamapi.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/steamapi.rs b/src/steamapi.rs index a594013..784dac5 100644 --- a/src/steamapi.rs +++ b/src/steamapi.rs @@ -3,9 +3,7 @@ use reqwest::{Url, cookie::{CookieStore}, header::COOKIE, header::{SET_COOKIE, U use rsa::{PublicKey, RsaPublicKey}; use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Serialize, Deserialize}; -use serde::de::{Visitor}; use rand::rngs::OsRng; -use std::fmt; use log::*; #[derive(Debug, Clone, Deserialize)] @@ -334,8 +332,6 @@ pub fn get_server_time() -> i64 { .send(); let value: serde_json::Value = resp.unwrap().json().unwrap(); - // println!("{}", value["response"]); - return String::from(value["response"]["server_time"].as_str().unwrap()).parse().unwrap(); } @@ -343,6 +339,9 @@ fn encrypt_password(rsa_resp: RsaResponse, password: &String) -> 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(); let public_key = RsaPublicKey::new(rsa_modulus, rsa_exponent).unwrap(); + #[cfg(test)] + let mut rng = rand::rngs::mock::StepRng::new(2, 1); + #[cfg(not(test))] let mut rng = OsRng; let padding = rsa::PaddingScheme::new_pkcs1v15_encrypt(); let encrypted_password = base64::encode(public_key.encrypt(&mut rng, padding, password.as_bytes()).unwrap()); @@ -359,5 +358,6 @@ fn test_encrypt_password() { token_gid: String::from("asdf"), }; let result = encrypt_password(rsa_resp, &String::from("kelwleofpsm3n4ofc")); - assert_eq!(result.len(), 344); // can't test exact match because the result is different every time (because of OsRng) + assert_eq!(result.len(), 344); + assert_eq!(result, "RUo/3IfbkVcJi1q1S5QlpKn1mEn3gNJoc/Z4VwxRV9DImV6veq/YISEuSrHB3885U5MYFLn1g94Y+cWRL6HGXoV+gOaVZe43m7O92RwiVz6OZQXMfAv3UC/jcqn/xkitnj+tNtmx55gCxmGbO2KbqQ0TQqAyqCOOw565B+Cwr2OOorpMZAViv9sKA/G3Q6yzscU6rhua179c8QjC1Hk3idUoSzpWfT4sHNBW/EREXZ3Dkjwu17xzpfwIUpnBVIlR8Vj3coHgUCpTsKVRA3T814v9BYPlvLYwmw5DW3ddx+2SyTY0P5uuog36TN2PqYS7ioF5eDe16gyfRR4Nzn/7wA=="); } From e0a668fa1e4aaee5de92930db281731b1fe32b29 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 10:55:55 -0400 Subject: [PATCH 2/9] change LoginResult to Result --- src/main.rs | 8 ++++---- src/steamapi.rs | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index 98bed8f..aaa50c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -345,18 +345,18 @@ fn do_login(account: &mut SteamGuardAccount) { let mut loops = 0; loop { match login.login() { - steamapi::LoginResult::Ok(s) => { + Ok(s) => { account.session = Option::Some(s); break; } - steamapi::LoginResult::Need2FA => { + Err(steamapi::LoginError::Need2FA) => { let server_time = steamapi::get_server_time(); login.twofactor_code = account.generate_code(server_time); } - steamapi::LoginResult::NeedCaptcha{ captcha_gid } => { + Err(steamapi::LoginError::NeedCaptcha{ captcha_gid }) => { login.captcha_text = prompt_captcha_text(&captcha_gid); } - steamapi::LoginResult::NeedEmail => { + Err(steamapi::LoginError::NeedEmail) => { println!("You should have received an email with a code."); print!("Enter code"); login.email_code = prompt(); diff --git a/src/steamapi.rs b/src/steamapi.rs index 784dac5..b98152e 100644 --- a/src/steamapi.rs +++ b/src/steamapi.rs @@ -48,8 +48,7 @@ struct RsaResponse { } #[derive(Debug)] -pub enum LoginResult { - Ok(Session), +pub enum LoginError { BadRSA, BadCredentials, NeedCaptcha{ captcha_gid: String }, @@ -95,6 +94,7 @@ impl UserLogin { } } + /// Updates the cookie jar with the session cookies by pinging steam servers. fn update_session(&self) { trace!("UserLogin::update_session"); let url = "https://steamcommunity.com".parse::().unwrap(); @@ -116,10 +116,10 @@ impl UserLogin { trace!("cookies: {:?}", self.cookies); } - pub fn login(&mut self) -> LoginResult { + pub fn login(&mut self) -> anyhow::Result { trace!("UserLogin::login"); if self.captcha_required && self.captcha_text.len() == 0 { - return LoginResult::NeedCaptcha{captcha_gid: self.captcha_gid.clone()}; + return Err(LoginError::NeedCaptcha{captcha_gid: self.captcha_gid.clone()}); } let url = "https://steamcommunity.com".parse::().unwrap(); @@ -145,7 +145,7 @@ impl UserLogin { } Err(error) => { error!("rsa error: {:?}", error); - return LoginResult::BadRSA + return Err(LoginError::BadRSA); } } @@ -183,40 +183,40 @@ impl UserLogin { Err(error) => { debug!("login response did not have normal schema"); error!("login parse error: {:?}", error); - return LoginResult::OtherFailure; + return Err(LoginError::OtherFailure); } } } Err(error) => { error!("login request error: {:?}", error); - return LoginResult::OtherFailure; + return Err(LoginError::OtherFailure); } } if login_resp.message.contains("too many login") { - return LoginResult::TooManyAttempts; + return Err(LoginError::TooManyAttempts); } if login_resp.message.contains("Incorrect login") { - return LoginResult::BadCredentials; + return Err(LoginError::BadCredentials); } if login_resp.captcha_needed { self.captcha_gid = login_resp.captcha_gid.clone(); - return LoginResult::NeedCaptcha{ captcha_gid: self.captcha_gid.clone() }; + return Err(LoginError::NeedCaptcha{ captcha_gid: self.captcha_gid.clone() }); } if login_resp.emailauth_needed { self.steam_id = login_resp.emailsteamid.clone(); - return LoginResult::NeedEmail; + return Err(LoginError::NeedEmail); } if login_resp.requires_twofactor { - return LoginResult::Need2FA; + return Err(LoginError::Need2FA); } if !login_resp.login_complete { - return LoginResult::BadCredentials; + return Err(LoginError::BadCredentials); } @@ -254,7 +254,7 @@ impl UserLogin { } _ => { error!("did not receive transfer_urls and transfer_parameters"); - return LoginResult::OtherFailure; + return Err(LoginError::OtherFailure); } } @@ -271,7 +271,7 @@ impl UserLogin { trace!("cookies {:?}", cookies); let session = self.build_session(oauth, session_id); - return LoginResult::Ok(session); + return Ok(session); } fn build_session(&self, data: OAuthData, session_id: String) -> Session { From 9aea25ea328c6834b6f5959bc9ca4b873894e58c Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 10:57:02 -0400 Subject: [PATCH 3/9] change import --- src/steamapi.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/steamapi.rs b/src/steamapi.rs index b98152e..5b20f21 100644 --- a/src/steamapi.rs +++ b/src/steamapi.rs @@ -3,7 +3,6 @@ use reqwest::{Url, cookie::{CookieStore}, header::COOKIE, header::{SET_COOKIE, U use rsa::{PublicKey, RsaPublicKey}; use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Serialize, Deserialize}; -use rand::rngs::OsRng; use log::*; #[derive(Debug, Clone, Deserialize)] @@ -342,7 +341,7 @@ fn encrypt_password(rsa_resp: RsaResponse, password: &String) -> String { #[cfg(test)] let mut rng = rand::rngs::mock::StepRng::new(2, 1); #[cfg(not(test))] - let mut rng = OsRng; + let mut rng = rand::rngs::OsRng; let padding = rsa::PaddingScheme::new_pkcs1v15_encrypt(); let encrypted_password = base64::encode(public_key.encrypt(&mut rng, padding, password.as_bytes()).unwrap()); return encrypted_password; From 66731164d671c5652a73896375bf41cb68e0af43 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 11:00:22 -0400 Subject: [PATCH 4/9] remove commented code --- src/lib.rs | 4 ---- src/steamapi.rs | 5 ----- 2 files changed, 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b51c5ac..79c893b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,9 +101,7 @@ impl SteamGuardAccount { let time_bytes: [u8; 8] = build_time_bytes(time / 30i64); let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()); - // println!("time_bytes: {:?}", time_bytes); let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes); - // println!("hashed_data: {:?}", hashed_data); let mut code_array: [u8; 5] = [0; 5]; let b = (hashed_data[19] & 0xF) as usize; let mut code_point: i32 = @@ -117,8 +115,6 @@ impl SteamGuardAccount { code_point /= steam_guard_code_translations.len() as i32; } - // println!("code_array: {:?}", code_array); - return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap() } diff --git a/src/steamapi.rs b/src/steamapi.rs index 5b20f21..f97022e 100644 --- a/src/steamapi.rs +++ b/src/steamapi.rs @@ -10,8 +10,6 @@ struct LoginResponse { success: bool, #[serde(default)] login_complete: bool, - // #[serde(default)] - // oauth: String, #[serde(default)] captcha_needed: bool, #[serde(default)] @@ -69,7 +67,6 @@ pub struct UserLogin { pub steam_id: u64, cookies: reqwest::cookie::Jar, - // cookies: Arc, client: reqwest::blocking::Client, } @@ -85,7 +82,6 @@ impl UserLogin { email_code: String::from(""), steam_id: 0, cookies: reqwest::cookie::Jar::default(), - // cookies: Arc::::new(reqwest::cookie::Jar::default()), client: reqwest::blocking::ClientBuilder::new() .cookie_store(true) .build() @@ -257,7 +253,6 @@ impl UserLogin { } } - // let oauth: OAuthData = serde_json::from_str(login_resp.oauth.as_str()).unwrap(); let url = "https://steamcommunity.com".parse::().unwrap(); let cookies = self.cookies.cookies(&url).unwrap(); let all_cookies = cookies.to_str().unwrap(); From 7c58e28d5651a976cd91bd2fd62a45ed131dad66 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 11:08:41 -0400 Subject: [PATCH 5/9] clean up imports --- src/accountlinker.rs | 4 ++-- src/main.rs | 13 +++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/accountlinker.rs b/src/accountlinker.rs index 12faa1f..51f6fa0 100644 --- a/src/accountlinker.rs +++ b/src/accountlinker.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use reqwest::{Url, cookie::{CookieStore}, header::COOKIE, header::{SET_COOKIE, USER_AGENT}}; -use serde::{Serialize, Deserialize}; +use reqwest::{Url, cookie::{CookieStore}, header::COOKIE}; +use serde::Deserialize; use serde_json::Value; use steamguard_cli::{SteamGuardAccount, steamapi::Session}; use log::*; diff --git a/src/main.rs b/src/main.rs index aaa50c1..c75d8bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,11 @@ extern crate rpassword; -use borrow::BorrowMut; -use collections::HashSet; -use io::{Write, stdout}; -use steamapi::Session; use steamguard_cli::*; -use termion::{color::Color, raw::IntoRawMode, screen::AlternateScreen}; -use ::std::*; -use text_io::read; -use std::{convert::TryInto, io::stdin, path::Path, sync::Arc}; +use std::collections::HashSet; +use std::{io::{Write, stdout, stdin}, path::Path}; use clap::{App, Arg, crate_version}; use log::*; use regex::Regex; -use termion::event::{Key, Event}; -use termion::input::{TermRead}; +use termion::{raw::IntoRawMode, screen::AlternateScreen, event::{Key, Event}, input::{TermRead}}; #[macro_use] extern crate lazy_static; From fcc56d6d7314cce323e274252b57f13700bbb247 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 11:44:56 -0400 Subject: [PATCH 6/9] change parse_shared_secret to not panic, add test for build_time_bytes --- src/lib.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 79c893b..58c5850 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,18 +55,10 @@ fn build_time_bytes(mut time: i64) -> [u8; 8] { return bytes } -pub fn parse_shared_secret(secret: String) -> [u8; 20] { - if secret.len() == 0 { - panic!("unable to parse empty shared secret") - } - match base64::decode(secret) { - Result::Ok(v) => { - return v.try_into().unwrap() - } - _ => { - panic!("unable to parse shared secret") - } - } +pub fn parse_shared_secret(secret: String) -> anyhow::Result<[u8; 20]> { + ensure!(secret.len() != 0, "unable to parse empty shared secret"); + let result = base64::decode(secret)?.try_into(); + return Ok(result.unwrap()); } fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String { @@ -100,7 +92,7 @@ impl SteamGuardAccount { 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]; let time_bytes: [u8; 8] = build_time_bytes(time / 30i64); - let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()); + let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()).unwrap(); let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes); let mut code_array: [u8; 5] = [0; 5]; let b = (hashed_data[19] & 0xF) as usize; @@ -279,6 +271,13 @@ impl SteamGuardAccount { mod tests { use super::*; + #[test] + fn test_build_time_bytes() { + let t1 = build_time_bytes(1617591917i64); + let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109]; + assert!(t1.iter().zip(t2.iter()).all(|(a,b)| a == b), "Arrays are not equal, got {:?}", t1); + } + #[test] fn test_generate_code() { let mut account = SteamGuardAccount::new(); From 67a7b9b0ee781d8d1d1c8b0400afd7e263d15d5d Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 11:47:37 -0400 Subject: [PATCH 7/9] use to_be_bytes for build_time_bytes --- src/lib.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 58c5850..103c316 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,13 +46,8 @@ pub struct SteamGuardAccount { pub session: Option, } -fn build_time_bytes(mut time: i64) -> [u8; 8] { - let mut bytes: [u8; 8] = [0; 8]; - for i in (0..8).rev() { - bytes[i] = time as u8; - time >>= 8; - } - return bytes +fn build_time_bytes(time: i64) -> [u8; 8] { + return time.to_be_bytes(); } pub fn parse_shared_secret(secret: String) -> anyhow::Result<[u8; 20]> { From d6aedb771ccaa7980a1272f8657d1690458b3290 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 12:01:23 -0400 Subject: [PATCH 8/9] add comment --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 103c316..c5acf98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,6 +86,7 @@ impl SteamGuardAccount { pub fn generate_code(&self, time: i64) -> 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 shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()).unwrap(); let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes); From c6d9fd0d75ddca104d0c692b0628328eefdf0a34 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 31 Jul 2021 12:24:49 -0400 Subject: [PATCH 9/9] seperate cli stuff from library stuff, and put it in another crate --- Cargo.lock | 23 +++++++++++++++++++++++ Cargo.toml | 7 +++++++ src/accountlinker.rs | 2 +- src/accountmanager.rs | 2 +- src/main.rs | 2 +- steamguard/Cargo.toml | 23 +++++++++++++++++++++++ {src => steamguard/src}/confirmation.rs | 0 {src => steamguard/src}/lib.rs | 0 {src => steamguard/src}/steamapi.rs | 0 9 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 steamguard/Cargo.toml rename {src => steamguard/src}/confirmation.rs (100%) rename {src => steamguard/src}/lib.rs (100%) rename {src => steamguard/src}/steamapi.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 80a1ce1..09b3e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "adler" version = "1.0.2" @@ -1241,6 +1243,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "steamguard" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "cookie", + "hmac-sha1", + "lazy_static 1.4.0", + "log", + "rand", + "regex", + "reqwest", + "rsa", + "serde", + "serde_json", + "standback", + "uuid", +] + [[package]] name = "steamguard-cli" version = "0.2.0" @@ -1261,6 +1283,7 @@ dependencies = [ "serde_json", "standback", "stderrlog", + "steamguard", "termion", "text_io", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 88bdf7e..8026e97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,9 @@ +[workspace] + +members = [ + "steamguard" +] + [package] name = "steamguard-cli" version = "0.2.0" @@ -26,3 +32,4 @@ regex = "1" lazy_static = "1.4.0" uuid = { version = "0.8", features = ["v4"] } termion = "1.5.6" +steamguard = { path = "./steamguard" } diff --git a/src/accountlinker.rs b/src/accountlinker.rs index 51f6fa0..5e039c6 100644 --- a/src/accountlinker.rs +++ b/src/accountlinker.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use reqwest::{Url, cookie::{CookieStore}, header::COOKIE}; use serde::Deserialize; use serde_json::Value; -use steamguard_cli::{SteamGuardAccount, steamapi::Session}; +use steamguard::{SteamGuardAccount, steamapi::Session}; use log::*; #[derive(Debug, Clone)] diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 8c36dea..625095f 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -3,7 +3,7 @@ use std::io::BufReader; use std::path::Path; use serde::{Serialize, Deserialize}; use std::error::Error; -use steamguard_cli::SteamGuardAccount; +use steamguard::SteamGuardAccount; use log::*; #[derive(Debug, Serialize, Deserialize)] diff --git a/src/main.rs b/src/main.rs index c75d8bf..15278e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ extern crate rpassword; -use steamguard_cli::*; +use steamguard::{SteamGuardAccount, Confirmation, ConfirmationType, steamapi}; use std::collections::HashSet; use std::{io::{Write, stdout, stdin}, path::Path}; use clap::{App, Arg, crate_version}; diff --git a/steamguard/Cargo.toml b/steamguard/Cargo.toml new file mode 100644 index 0000000..d31c89c --- /dev/null +++ b/steamguard/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "steamguard" +version = "0.1.0" +edition = "2018" +description = "Library for generating 2fa codes for Steam and responding to mobile confirmations." + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "^1.0" +hmac-sha1 = "^0.1" +base64 = "0.13.0" +reqwest = { version = "0.11", features = ["blocking", "json", "cookies", "gzip"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +rsa = "0.5.0" +rand = "0.8.4" +standback = "0.2.17" # required to fix a compilation error on a transient dependency +cookie = "0.14" +regex = "1" +lazy_static = "1.4.0" +uuid = { version = "0.8", features = ["v4"] } +log = "0.4.14" diff --git a/src/confirmation.rs b/steamguard/src/confirmation.rs similarity index 100% rename from src/confirmation.rs rename to steamguard/src/confirmation.rs diff --git a/src/lib.rs b/steamguard/src/lib.rs similarity index 100% rename from src/lib.rs rename to steamguard/src/lib.rs diff --git a/src/steamapi.rs b/steamguard/src/steamapi.rs similarity index 100% rename from src/steamapi.rs rename to steamguard/src/steamapi.rs