Merge pull request #156 from dyc3/offline-code-gen
Make it possible to generate codes offline
This commit is contained in:
commit
3f5fa9a033
6 changed files with 100 additions and 33 deletions
22
src/cli.rs
22
src/cli.rs
|
@ -34,6 +34,9 @@ pub(crate) struct Args {
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
pub sub: Option<Subcommands>,
|
pub sub: Option<Subcommands>,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
pub code: ArgsCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
@ -46,6 +49,7 @@ pub(crate) enum Subcommands {
|
||||||
Remove(ArgsRemove),
|
Remove(ArgsRemove),
|
||||||
Encrypt(ArgsEncrypt),
|
Encrypt(ArgsEncrypt),
|
||||||
Decrypt(ArgsDecrypt),
|
Decrypt(ArgsDecrypt),
|
||||||
|
Code(ArgsCode),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, ArgEnum)]
|
#[derive(Debug, Clone, Copy, ArgEnum)]
|
||||||
|
@ -143,3 +147,21 @@ pub(crate) struct ArgsEncrypt;
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[clap(about = "Decrypt all maFiles")]
|
#[clap(about = "Decrypt all maFiles")]
|
||||||
pub(crate) struct ArgsDecrypt;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
29
src/main.rs
29
src/main.rs
|
@ -1,6 +1,7 @@
|
||||||
extern crate rpassword;
|
extern crate rpassword;
|
||||||
use clap::{IntoApp, Parser};
|
use clap::{IntoApp, Parser};
|
||||||
use log::*;
|
use log::*;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdout, Write},
|
io::{stdout, Write},
|
||||||
path::Path,
|
path::Path,
|
||||||
|
@ -167,21 +168,20 @@ fn run() -> anyhow::Result<()> {
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
match args.sub {
|
match args.sub.unwrap_or(cli::Subcommands::Code(args.code)) {
|
||||||
Some(cli::Subcommands::Trade(args)) => {
|
cli::Subcommands::Trade(args) => {
|
||||||
return do_subcmd_trade(args, &mut manifest, selected_accounts);
|
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);
|
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);
|
error!("Unknown subcommand: {:?}", s);
|
||||||
return Err(errors::UserError::UnknownSubcommand.into());
|
return Err(errors::UserError::UnknownSubcommand.into());
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
debug!("No subcommand given, assuming user wants a 2fa code");
|
|
||||||
return do_subcmd_code(selected_accounts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ fn do_login_impl(
|
||||||
}
|
}
|
||||||
Err(LoginError::Need2FA) => match account {
|
Err(LoginError::Need2FA) => match account {
|
||||||
Some(a) => {
|
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);
|
login.twofactor_code = a.generate_code(server_time);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -662,8 +662,15 @@ fn do_subcmd_decrypt(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_subcmd_code(selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>>) -> anyhow::Result<()> {
|
fn do_subcmd_code(
|
||||||
let server_time = steamapi::get_server_time();
|
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);
|
debug!("Time used to generate codes: {}", server_time);
|
||||||
for account in selected_accounts {
|
for account in selected_accounts {
|
||||||
info!(
|
info!(
|
||||||
|
|
|
@ -82,7 +82,7 @@ impl AccountLinker {
|
||||||
account: &mut SteamGuardAccount,
|
account: &mut SteamGuardAccount,
|
||||||
sms_code: String,
|
sms_code: String,
|
||||||
) -> anyhow::Result<(), FinalizeLinkError> {
|
) -> 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 code = account.generate_code(time);
|
||||||
let resp: FinalizeAddAuthenticatorResponse =
|
let resp: FinalizeAddAuthenticatorResponse =
|
||||||
self.client
|
self.client
|
||||||
|
|
|
@ -62,11 +62,11 @@ pub struct SteamGuardAccount {
|
||||||
pub session: Option<secrecy::Secret<steamapi::Session>>,
|
pub session: Option<secrecy::Secret<steamapi::Session>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_time_bytes(time: i64) -> [u8; 8] {
|
fn build_time_bytes(time: u64) -> [u8; 8] {
|
||||||
return time.to_be_bytes();
|
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 decode: &[u8] = &base64::decode(&identity_secret).unwrap();
|
||||||
let time_bytes = build_time_bytes(time);
|
let time_bytes = build_time_bytes(time);
|
||||||
let tag_bytes = tag.as_bytes();
|
let tag_bytes = tag.as_bytes();
|
||||||
|
@ -98,13 +98,12 @@ impl SteamGuardAccount {
|
||||||
self.session = Some(session.into());
|
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);
|
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 session = self.session.as_ref().unwrap().expose_secret();
|
||||||
let time = steamapi::get_server_time();
|
|
||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("p", self.device_id.clone());
|
params.insert("p", self.device_id.clone());
|
||||||
params.insert("a", session.steam_id.to_string());
|
params.insert("a", session.steam_id.to_string());
|
||||||
|
@ -145,12 +144,13 @@ impl SteamGuardAccount {
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
|
let time = steamapi::get_server_time()?.server_time;
|
||||||
let resp = client
|
let resp = client
|
||||||
.get("https://steamcommunity.com/mobileconf/conf".parse::<Url>().unwrap())
|
.get("https://steamcommunity.com/mobileconf/conf".parse::<Url>().unwrap())
|
||||||
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
.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(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())
|
.header(COOKIE, cookies.cookies(&url).unwrap())
|
||||||
.query(&self.get_confirmation_query_params("conf"))
|
.query(&self.get_confirmation_query_params("conf", time))
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
trace!("{:?}", resp);
|
trace!("{:?}", resp);
|
||||||
|
@ -173,7 +173,8 @@ impl SteamGuardAccount {
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()?;
|
.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("op", operation);
|
||||||
query_params.insert("cid", conf.id.to_string());
|
query_params.insert("cid", conf.id.to_string());
|
||||||
query_params.insert("ck", conf.key.to_string());
|
query_params.insert("ck", conf.key.to_string());
|
||||||
|
@ -217,7 +218,8 @@ impl SteamGuardAccount {
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()?;
|
.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::<Url>().unwrap())
|
let resp: ConfirmationDetailsResponse = client.get(format!("https://steamcommunity.com/mobileconf/details/{}", conf.id).parse::<Url>().unwrap())
|
||||||
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
||||||
|
|
|
@ -113,17 +113,53 @@ impl SerializableSecret for Session {}
|
||||||
impl CloneableSecret for Session {}
|
impl CloneableSecret for Session {}
|
||||||
impl DebugSecret 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<QueryTimeResponse> {
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let resp = client
|
let resp = client
|
||||||
.post("https://api.steampowered.com/ITwoFactorService/QueryTime/v0001")
|
.post("https://api.steampowered.com/ITwoFactorService/QueryTime/v0001")
|
||||||
.body("steamid=0")
|
.body("steamid=0")
|
||||||
.send();
|
.send()?;
|
||||||
let value: serde_json::Value = resp.unwrap().json().unwrap();
|
let resp: SteamApiResponse<QueryTimeResponse> = resp.json()?;
|
||||||
|
|
||||||
return String::from(value["response"]["server_time"].as_str().unwrap())
|
return Ok(resp.response);
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
/// 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,
|
&self,
|
||||||
sms_code: String,
|
sms_code: String,
|
||||||
code_2fa: String,
|
code_2fa: String,
|
||||||
time_2fa: i64,
|
time_2fa: u64,
|
||||||
) -> anyhow::Result<FinalizeAddAuthenticatorResponse> {
|
) -> anyhow::Result<FinalizeAddAuthenticatorResponse> {
|
||||||
ensure!(matches!(self.session, Some(_)));
|
ensure!(matches!(self.session, Some(_)));
|
||||||
let params = hashmap! {
|
let params = hashmap! {
|
||||||
|
|
|
@ -18,14 +18,14 @@ impl TwoFactorSecret {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a 5 character 2FA code to that can be used to log in to Steam.
|
/// 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] = [
|
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,
|
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,
|
86, 87, 88, 89,
|
||||||
];
|
];
|
||||||
|
|
||||||
// this effectively makes it so that it creates a new code every 30 seconds.
|
// 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 hashed_data = hmacsha1::hmac_sha1(self.0.expose_secret(), &time_bytes);
|
||||||
let mut code_array: [u8; 5] = [0; 5];
|
let mut code_array: [u8; 5] = [0; 5];
|
||||||
let b = (hashed_data[19] & 0xF) as usize;
|
let b = (hashed_data[19] & 0xF) as usize;
|
||||||
|
@ -70,7 +70,7 @@ impl PartialEq for TwoFactorSecret {
|
||||||
|
|
||||||
impl Eq 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();
|
return time.to_be_bytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ mod tests {
|
||||||
let secret: FooBar =
|
let secret: FooBar =
|
||||||
serde_json::from_str(&"{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}")?;
|
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");
|
assert_eq!(code, "2F9J5");
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -130,7 +130,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_time_bytes() {
|
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];
|
let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109];
|
||||||
assert!(
|
assert!(
|
||||||
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
|
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
|
||||||
|
@ -143,7 +143,7 @@ mod tests {
|
||||||
fn test_generate_code() -> anyhow::Result<()> {
|
fn test_generate_code() -> anyhow::Result<()> {
|
||||||
let secret = TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?;
|
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");
|
assert_eq!(code, "2F9J5");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue