Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -181,6 +183,7 @@ pub(crate) enum Subcommands {
Qr(QrCommand),
QrLogin(QrLoginCommand),
Status(StatusCommand),
UpgradeToken(UpgradeTokenCommand),
}

#[derive(Debug, Clone, Copy, ValueEnum)]
Expand Down
148 changes: 148 additions & 0 deletions src/commands/upgradetoken.rs
Original file line number Diff line number Diff line change
@@ -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<T> AccountCommand<T> for UpgradeTokenCommand
where
T: Transport + Clone,
{
fn execute(
&self,
transport: T,
manager: &mut AccountManager,
accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
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<T: Transport>(
&self,
client: &TwoFactorClient<T>,
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<T: Transport>(
&self,
client: &TwoFactorClient<T>,
account: &SteamGuardAccount,
) -> Result<u32, UpgradeTokenError> {
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::<Sha1>::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),
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion steamguard/src/confirmation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
16 changes: 16 additions & 0 deletions steamguard/src/steamapi/twofactor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ where
.send_request::<CTwoFactor_Time_Request, CTwoFactor_Time_Response>(req)?;
Ok(resp)
}

pub fn update_token_version(
&self,
req: CTwoFactor_UpdateTokenVersion_Request,
access_token: &Jwt,
) -> Result<ApiResponse<CTwoFactor_UpdateTokenVersion_Response>, TransportError> {
let req = ApiRequest::new(SERVICE_NAME, "UpdateTokenVersion", 1, req)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CTwoFactor_UpdateTokenVersion_Request, CTwoFactor_UpdateTokenVersion_Response>(
req,
)?;
Ok(resp)
}
}

macro_rules! impl_buildable_req {
Expand Down Expand Up @@ -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);