From b6d8a102b4e69276c4eb7f5fc0976fe2fa638d2c Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 20 Mar 2025 12:41:56 -0400 Subject: [PATCH] feat(upgrade-token): add `upgrade-token` command --- Cargo.lock | 2 + Cargo.toml | 2 + src/commands.rs | 5 +- src/commands/upgradetoken.rs | 148 +++++++++++++++++++++++++++ src/main.rs | 1 + steamguard/src/confirmation.rs | 2 +- steamguard/src/steamapi/twofactor.rs | 16 +++ 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/commands/upgradetoken.rs diff --git a/Cargo.lock b/Cargo.lock index d20c7a7f..e31ee787 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3196,6 +3196,7 @@ dependencies = [ "crossterm", "dirs", "gethostname", + "hmac", "image", "inout", "keyring", @@ -3217,6 +3218,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "sha1", + "sha2", "stderrlog", "steamguard", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index d933690b..01c14fb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,8 @@ sha1 = "0.10.5" rayon = "1.7.0" rqrr = "0.7.1" image = "0.25" +hmac = "^0.12" +sha2 = "^0.10" [dev-dependencies] tempfile = "3" diff --git a/src/commands.rs b/src/commands.rs index 8f6b86f1..752af4c2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -23,6 +23,7 @@ pub mod qr_login; pub mod remove; pub mod setup; pub mod status; +pub mod upgradetoken; pub use approve::ApproveCommand; pub use code::CodeCommand; @@ -36,7 +37,8 @@ pub use import::ImportCommand; pub use qr::QrCommand; pub use qr_login::QrLoginCommand; pub use remove::RemoveCommand; -pub use setup::SetupCommand; // export new command +pub use setup::SetupCommand; +pub use upgradetoken::UpgradeTokenCommand; /// A command that does not operate on the manifest or individual accounts. pub(crate) trait ConstCommand { @@ -181,6 +183,7 @@ pub(crate) enum Subcommands { Qr(QrCommand), QrLogin(QrLoginCommand), Status(StatusCommand), + UpgradeToken(UpgradeTokenCommand), } #[derive(Debug, Clone, Copy, ValueEnum)] diff --git a/src/commands/upgradetoken.rs b/src/commands/upgradetoken.rs new file mode 100644 index 00000000..2141e3d0 --- /dev/null +++ b/src/commands/upgradetoken.rs @@ -0,0 +1,148 @@ +use crate::commands::AccountCommand; +use crate::{commands::GlobalArgs, AccountManager}; +use base64::Engine; +use clap::Parser; +use hmac::{Hmac, Mac}; +use log::*; +use secrecy::ExposeSecret; +use sha1::Sha1; +use sha2::Sha256; +use std::sync::{Arc, Mutex}; +use steamguard::protobufs::service_twofactor::{ + CTwoFactor_Status_Request, CTwoFactor_UpdateTokenVersion_Request, +}; +use steamguard::steamapi::{EResult, TwoFactorClient}; +use steamguard::transport::{Transport, TransportError}; +use steamguard::SteamGuardAccount; +use thiserror::Error; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Upgrade the token version for accounts.")] +pub struct UpgradeTokenCommand; + +impl AccountCommand for UpgradeTokenCommand +where + T: Transport + Clone, +{ + fn execute( + &self, + transport: T, + manager: &mut AccountManager, + accounts: Vec>>, + args: &GlobalArgs, + ) -> anyhow::Result<()> { + let twofactor = TwoFactorClient::new(transport.clone()); + for account in accounts { + let mut account = account.lock().unwrap(); + if !account.is_logged_in() { + info!("Account does not have tokens, logging in"); + crate::do_login(transport.clone(), &mut account, args.password.clone())?; + } + + // check the version of the token + let version = self.get_token_version(&twofactor, &account)?; + if version == 2 { + info!( + "Token for {} is already at version 2. Nothing to do.", + account.account_name + ); + continue; + } + + let result = self.upgrade_token(&twofactor, &account); + match result { + Ok(_) => { + info!("Successfully upgraded token for {}", account.account_name); + } + Err(e) => { + error!( + "Failed to upgrade token for {}: {}", + account.account_name, e + ); + } + } + } + manager.save()?; + Ok(()) + } +} + +impl UpgradeTokenCommand { + /// Upgrade the token version for the given account to version 2. + fn upgrade_token( + &self, + client: &TwoFactorClient, + account: &SteamGuardAccount, + ) -> Result<(), UpgradeTokenError> { + let access_token = account + .tokens + .as_ref() + .ok_or(TransportError::Unauthorized)? + .access_token(); + let signature = build_upgrade_signature(account, 2); + + let mut req = CTwoFactor_UpdateTokenVersion_Request::new(); + req.set_steamid(account.steam_id); + req.set_version(2); + req.set_signature(signature.to_vec()); + + let resp = client.update_token_version(req, access_token)?; + // Thankfully, we don't need to save anything from this response since it's empty. + + match resp.result() { + EResult::OK => Ok(()), + EResult::InvalidSignature => Err(UpgradeTokenError::InvalidSignature), + err => Err(UpgradeTokenError::Unknown(err).into()), + } + } + + fn get_token_version( + &self, + client: &TwoFactorClient, + account: &SteamGuardAccount, + ) -> Result { + let access_token = account + .tokens + .as_ref() + .ok_or(TransportError::Unauthorized)? + .access_token(); + + let mut req = CTwoFactor_Status_Request::new(); + req.set_steamid(account.steam_id); + let resp = client.query_status(req, access_token)?; + + let data = resp.into_response_data(); + + Ok(data.version()) + } +} + +/// Reverse engineered from the Steam mobile app. (WIP: does not work yet) +/// +/// Pretty confident that it uses Sha1 for the HMAC. +fn build_upgrade_signature(account: &SteamGuardAccount, version: u32) -> [u8; 32] { + let mut buffer = [0u8; 12]; + buffer[0..4].copy_from_slice(&version.to_le_bytes()); + buffer[4..12].copy_from_slice(&account.steam_id.to_le_bytes()); + + let mut mac = Hmac::::new_from_slice(&buffer).unwrap(); + // mac.update(account.shared_secret.expose_secret()); + let decode: &[u8] = &base64::engine::general_purpose::STANDARD + .decode(account.identity_secret.expose_secret()) + .unwrap(); + mac.update(decode); + let result = mac.finalize(); + result.into_bytes().into() +} + +#[derive(Debug, Error)] +enum UpgradeTokenError { + #[error( + "The signature we provided was invalid. This is a bug in steamguard-cli, please report it!" + )] + InvalidSignature, + #[error("Steam returned an unexpected error code when upgrading the 2fa token: {0:?}")] + Unknown(EResult), + #[error("Transport error: {0}")] + TransportError(#[from] TransportError), +} diff --git a/src/main.rs b/src/main.rs index c16f76af..3f35f2e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,7 @@ fn run(args: commands::Args) -> anyhow::Result<()> { Subcommands::Qr(args) => CommandType::Account(Box::new(args)), Subcommands::QrLogin(args) => CommandType::Account(Box::new(args)), Subcommands::Status(args) => CommandType::Account(Box::new(args)), + Subcommands::UpgradeToken(args) => CommandType::Account(Box::new(args)), }; if let CommandType::Const(cmd) = cmd { diff --git a/steamguard/src/confirmation.rs b/steamguard/src/confirmation.rs index 2a8ea842..9ac85238 100644 --- a/steamguard/src/confirmation.rs +++ b/steamguard/src/confirmation.rs @@ -459,7 +459,7 @@ fn generate_confirmation_hash_for_time( mac.update(&build_time_bytes(time)); mac.update(tag.as_bytes()); let result = mac.finalize(); - let hash = result.into_bytes(); + let hash: [u8; 20] = result.into_bytes().into(); base64::engine::general_purpose::STANDARD.encode(hash) } diff --git a/steamguard/src/steamapi/twofactor.rs b/steamguard/src/steamapi/twofactor.rs index adb30b1c..5d8b91b4 100644 --- a/steamguard/src/steamapi/twofactor.rs +++ b/steamguard/src/steamapi/twofactor.rs @@ -128,6 +128,21 @@ where .send_request::(req)?; Ok(resp) } + + pub fn update_token_version( + &self, + req: CTwoFactor_UpdateTokenVersion_Request, + access_token: &Jwt, + ) -> Result, TransportError> { + let req = ApiRequest::new(SERVICE_NAME, "UpdateTokenVersion", 1, req) + .with_access_token(access_token); + let resp = self + .transport + .send_request::( + req, + )?; + Ok(resp) + } } macro_rules! impl_buildable_req { @@ -157,3 +172,4 @@ impl_buildable_req!( ); impl_buildable_req!(CTwoFactor_Status_Request, true); impl_buildable_req!(CTwoFactor_Time_Request, false); +impl_buildable_req!(CTwoFactor_UpdateTokenVersion_Request, true);