2021-04-04 20:07:06 +02:00
|
|
|
use std::{collections::HashMap, convert::TryInto, thread, time};
|
|
|
|
use hmacsha1::hmac_sha1;
|
2021-04-04 23:48:44 +02:00
|
|
|
use regex::Regex;
|
2021-04-04 20:07:06 +02:00
|
|
|
use reqwest::{Url, cookie::CookieStore, header::{COOKIE, USER_AGENT}};
|
2021-03-26 18:32:37 +01:00
|
|
|
use serde::{Serialize, Deserialize};
|
2021-04-04 20:07:06 +02:00
|
|
|
use log::*;
|
2021-04-04 23:48:44 +02:00
|
|
|
#[macro_use]
|
|
|
|
extern crate lazy_static;
|
2021-03-26 18:32:37 +01:00
|
|
|
|
2021-03-30 21:51:26 +02:00
|
|
|
pub mod steamapi;
|
2021-03-22 02:21:29 +01:00
|
|
|
|
|
|
|
// const STEAMAPI_BASE: String = "https://api.steampowered.com";
|
|
|
|
// const COMMUNITY_BASE: String = "https://steamcommunity.com";
|
|
|
|
// const MOBILEAUTH_BASE: String = STEAMAPI_BASE + "/IMobileAuthService/%s/v0001";
|
|
|
|
// static MOBILEAUTH_GETWGTOKEN: String = MOBILEAUTH_BASE.Replace("%s", "GetWGToken");
|
|
|
|
// const TWO_FACTOR_BASE: String = STEAMAPI_BASE + "/ITwoFactorService/%s/v0001";
|
|
|
|
// static TWO_FACTOR_TIME_QUERY: String = TWO_FACTOR_BASE.Replace("%s", "QueryTime");
|
|
|
|
|
2021-04-04 23:48:44 +02:00
|
|
|
lazy_static! {
|
|
|
|
static ref CONFIRMATION_REGEX: Regex = Regex::new("<div class=\"mobileconf_list_entry\" id=\"conf[0-9]+\" data-confid=\"(\\d+)\" data-key=\"(\\d+)\" data-type=\"(\\d+)\" data-creator=\"(\\d+)\"").unwrap();
|
|
|
|
static ref CONFIRMATION_DESCRIPTION_REGEX: Regex = Regex::new("<div>((Confirm|Trade|Account recovery|Sell -) .+)</div>").unwrap();
|
|
|
|
}
|
|
|
|
|
2021-03-22 02:21:29 +01:00
|
|
|
extern crate hmacsha1;
|
|
|
|
extern crate base64;
|
2021-03-30 21:51:26 +02:00
|
|
|
extern crate cookie;
|
2021-03-22 02:21:29 +01:00
|
|
|
|
2021-03-27 17:14:34 +01:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
2021-03-22 02:21:29 +01:00
|
|
|
pub struct SteamGuardAccount {
|
|
|
|
pub account_name: String,
|
2021-03-26 19:31:48 +01:00
|
|
|
pub serial_number: String,
|
2021-03-22 02:21:29 +01:00
|
|
|
pub revocation_code: String,
|
2021-03-26 18:32:37 +01:00
|
|
|
pub shared_secret: String,
|
2021-03-26 19:31:48 +01:00
|
|
|
pub token_gid: String,
|
|
|
|
pub identity_secret: String,
|
|
|
|
pub server_time: u64,
|
|
|
|
pub uri: String,
|
|
|
|
pub fully_enrolled: bool,
|
2021-04-04 20:07:06 +02:00
|
|
|
pub device_id: String,
|
2021-03-26 18:32:37 +01:00
|
|
|
#[serde(rename = "Session")]
|
|
|
|
pub session: Option<steamapi::Session>,
|
2021-03-22 02:21:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fn build_time_bytes(mut time: i64) -> [u8; 8] {
|
|
|
|
time /= 30i64;
|
|
|
|
|
|
|
|
let mut bytes: [u8; 8] = [0; 8];
|
|
|
|
for i in (0..8).rev() {
|
|
|
|
bytes[i] = time as u8;
|
|
|
|
time >>= 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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SteamGuardAccount {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
return SteamGuardAccount{
|
|
|
|
account_name: String::from(""),
|
2021-03-26 19:31:48 +01:00
|
|
|
serial_number: String::from(""),
|
2021-03-22 02:21:29 +01:00
|
|
|
revocation_code: String::from(""),
|
2021-03-26 18:32:37 +01:00
|
|
|
shared_secret: String::from(""),
|
2021-03-26 19:31:48 +01:00
|
|
|
token_gid: String::from(""),
|
|
|
|
identity_secret: String::from(""),
|
|
|
|
server_time: 0,
|
|
|
|
uri: String::from(""),
|
|
|
|
fully_enrolled: false,
|
2021-04-04 20:07:06 +02:00
|
|
|
device_id: String::from(""),
|
2021-03-26 18:32:37 +01:00
|
|
|
session: Option::None,
|
2021-03-22 02:21:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
|
|
let time_bytes: [u8; 8] = build_time_bytes(time);
|
2021-03-26 18:32:37 +01:00
|
|
|
let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone());
|
2021-03-22 02:21:29 +01:00
|
|
|
// println!("time_bytes: {:?}", time_bytes);
|
2021-03-26 18:32:37 +01:00
|
|
|
let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes);
|
2021-03-22 02:21:29 +01:00
|
|
|
// 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 =
|
|
|
|
((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);
|
|
|
|
|
|
|
|
for i in 0..5 {
|
|
|
|
code_array[i] = steam_guard_code_translations[code_point as usize % steam_guard_code_translations.len()];
|
|
|
|
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()
|
|
|
|
}
|
2021-04-04 20:07:06 +02:00
|
|
|
|
|
|
|
fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> {
|
|
|
|
let session = self.session.clone().unwrap();
|
|
|
|
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());
|
|
|
|
params.insert("k", self.generate_confirmation_hash_for_time(time, tag));
|
|
|
|
params.insert("t", time.to_string());
|
|
|
|
params.insert("m", String::from("android"));
|
|
|
|
params.insert("tag", String::from(tag));
|
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn generate_confirmation_hash_for_time(&self, time: i64, tag: &str) -> String {
|
|
|
|
let decode: &[u8] = &base64::decode(&self.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_trade_confirmations(&self) {
|
|
|
|
// uri: "https://steamcommunity.com/mobileconf/conf"
|
|
|
|
// confirmation details:
|
|
|
|
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
|
|
|
let cookies = reqwest::cookie::Jar::default();
|
2021-04-04 23:48:44 +02:00
|
|
|
let session = self.session.clone().unwrap();
|
|
|
|
let session_id = session.session_id;
|
|
|
|
cookies.add_cookie_str("mobileClientVersion=0 (2.1.3)", &url);
|
|
|
|
cookies.add_cookie_str("mobileClient=android", &url);
|
|
|
|
cookies.add_cookie_str("Steam_Language=english", &url);
|
|
|
|
cookies.add_cookie_str("dob=", &url);
|
|
|
|
cookies.add_cookie_str(format!("sessionid={}", session_id).as_str(), &url);
|
|
|
|
cookies.add_cookie_str(format!("steamid={}", session.steam_id).as_str(), &url);
|
|
|
|
cookies.add_cookie_str(format!("steamLogin={}", session.steam_login).as_str(), &url);
|
|
|
|
cookies.add_cookie_str(format!("steamLoginSecure={}", session.steam_login_secure).as_str(), &url);
|
2021-04-04 20:07:06 +02:00
|
|
|
let client = reqwest::blocking::ClientBuilder::new()
|
2021-04-04 23:48:44 +02:00
|
|
|
.cookie_store(true)
|
2021-04-04 20:07:06 +02:00
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
2021-04-04 23:48:44 +02:00
|
|
|
match client
|
|
|
|
.get("https://steamcommunity.com/mobileconf/conf".parse::<Url>().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"))
|
|
|
|
.send() {
|
|
|
|
Ok(resp) => {
|
|
|
|
trace!("{:?}", resp);
|
|
|
|
let text = resp.text().unwrap();
|
|
|
|
trace!("text: {:?}", text);
|
|
|
|
match CONFIRMATION_REGEX.captures(text.as_str()) {
|
|
|
|
Some(caps) => {
|
|
|
|
let conf_id = &caps[1];
|
|
|
|
let conf_key = &caps[2];
|
|
|
|
let conf_type = &caps[3];
|
|
|
|
let conf_creator = &caps[4];
|
|
|
|
debug!("{} {} {} {}", conf_id, conf_key, conf_type, conf_creator);
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
info!("No confirmations");
|
|
|
|
}
|
2021-04-04 20:07:06 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-04 23:48:44 +02:00
|
|
|
Err(e) => {
|
|
|
|
error!("error: {:?}", e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2021-04-04 20:07:06 +02:00
|
|
|
}
|
2021-03-22 02:21:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_generate_code() {
|
|
|
|
let mut account = SteamGuardAccount::new();
|
2021-03-26 18:32:37 +01:00
|
|
|
account.shared_secret = String::from("zvIayp3JPvtvX/QGHqsqKBk/44s=");
|
2021-03-22 02:21:29 +01:00
|
|
|
|
|
|
|
let code = account.generate_code(1616374841i64);
|
|
|
|
assert_eq!(code, "2F9J5")
|
|
|
|
}
|
|
|
|
}
|