steamguard-cli/steamguard/src/accountlinker.rs
Carson McManus 1632e2f10e
add PhoneLinker, and the ability to add a phone number to the account during setup (#223)
- add proto for IPhoneService
- add PhoneClient
- add PhoneLinker
- fix lints and such
- add comments
- update phone linker
- use phonenumber crate for phone linker
- adjust errors for account linker
- update setup command to be able to add phone numbers
- adjust logging in the setup command
- update account linker
2023-06-25 17:11:24 +00:00

196 lines
6 KiB
Rust

use crate::protobufs::service_twofactor::{
CTwoFactor_AddAuthenticator_Request, CTwoFactor_FinalizeAddAuthenticator_Request,
};
use crate::steamapi::twofactor::TwoFactorClient;
use crate::token::TwoFactorSecret;
use crate::transport::WebApiTransport;
use crate::{steamapi::EResult, token::Tokens, SteamGuardAccount};
use log::*;
use thiserror::Error;
#[derive(Debug)]
pub struct AccountLinker {
device_id: String,
pub account: Option<SteamGuardAccount>,
pub finalized: bool,
tokens: Tokens,
client: TwoFactorClient<WebApiTransport>,
}
impl AccountLinker {
pub fn new(tokens: Tokens) -> AccountLinker {
Self {
device_id: generate_device_id(),
account: None,
finalized: false,
tokens,
client: TwoFactorClient::new(WebApiTransport::new()),
}
}
pub fn tokens(&self) -> &Tokens {
&self.tokens
}
pub fn link(&mut self) -> anyhow::Result<AccountLinkSuccess, AccountLinkError> {
let access_token = self.tokens.access_token();
let steam_id = access_token.decode()?.steam_id();
let mut req = CTwoFactor_AddAuthenticator_Request::new();
req.set_authenticator_type(1);
req.set_steamid(steam_id);
req.set_sms_phone_id("1".to_owned());
req.set_device_identifier(self.device_id.clone());
let resp = self.client.add_authenticator(req, access_token)?;
if resp.result != EResult::OK {
return Err(resp.result.into());
}
let mut resp = resp.into_response_data();
let account = SteamGuardAccount {
account_name: resp.take_account_name(),
steam_id,
serial_number: resp.serial_number().to_string(),
revocation_code: resp.take_revocation_code().into(),
uri: resp.take_uri().into(),
shared_secret: TwoFactorSecret::from_bytes(resp.take_shared_secret()),
token_gid: resp.take_token_gid(),
identity_secret: base64::encode(resp.take_identity_secret()).into(),
device_id: self.device_id.clone(),
secret_1: base64::encode(resp.take_secret_1()).into(),
tokens: Some(self.tokens.clone()),
};
let success = AccountLinkSuccess {
account,
server_time: resp.server_time(),
phone_number_hint: resp.take_phone_number_hint(),
};
Ok(success)
}
/// You may have to call this multiple times. If you have to call it a bunch of times, then you can assume that you are unable to generate correct 2fa codes.
pub fn finalize(
&mut self,
time: u64,
account: &mut SteamGuardAccount,
sms_code: String,
) -> anyhow::Result<(), FinalizeLinkError> {
let code = account.generate_code(time);
let token = self.tokens.access_token();
let steam_id = account.steam_id;
let mut req = CTwoFactor_FinalizeAddAuthenticator_Request::new();
req.set_steamid(steam_id);
req.set_authenticator_code(code);
req.set_authenticator_time(time);
req.set_activation_code(sms_code);
let resp = self.client.finalize_authenticator(req, token)?;
if resp.result != EResult::OK {
return Err(resp.result.into());
}
let resp = resp.into_response_data();
if resp.want_more() {
return Err(FinalizeLinkError::WantMore {
server_time: resp.server_time(),
});
}
self.finalized = true;
Ok(())
}
}
#[derive(Debug)]
pub struct AccountLinkSuccess {
account: SteamGuardAccount,
server_time: u64,
phone_number_hint: String,
}
impl AccountLinkSuccess {
pub fn account(&self) -> &SteamGuardAccount {
&self.account
}
pub fn into_account(self) -> SteamGuardAccount {
self.account
}
pub fn server_time(&self) -> u64 {
self.server_time
}
pub fn phone_number_hint(&self) -> &str {
&self.phone_number_hint
}
}
fn generate_device_id() -> String {
format!("android:{}", uuid::Uuid::new_v4())
}
#[derive(Error, Debug)]
pub enum AccountLinkError {
/// No phone number on the account
#[error("A phone number is needed, but not already present on the account.")]
MustProvidePhoneNumber,
/// User need to click link from confirmation email
#[error("An email has been sent to the user's email, click the link in that email.")]
MustConfirmEmail,
#[error("Authenticator is already present on this account.")]
AuthenticatorPresent,
#[error("You are sending too many requests to Steam, and we got rate limited. Wait at least a couple hours and try again.")]
RateLimitExceeded,
#[error("Steam was unable to link the authenticator to the account. No additional information about this error is available. This is a Steam error, not a steamguard-cli error. Try adding a phone number to your Steam account (which you can do here: https://store.steampowered.com/phone/add), or try again later.")]
GenericFailure,
#[error("Steam returned an unexpected error code: {0:?}")]
UnknownEResult(EResult),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}
impl From<EResult> for AccountLinkError {
fn from(result: EResult) -> Self {
match result {
EResult::RateLimitExceeded => AccountLinkError::RateLimitExceeded,
EResult::NoVerifiedPhone => AccountLinkError::MustProvidePhoneNumber,
EResult::DuplicateRequest => AccountLinkError::AuthenticatorPresent,
// If the user has no phone number on their account, it will always return this status code.
// However, this does not mean that this status just means "no phone number". It can also
// be literally anything else, so that's why we return GenericFailure here.
// update 2023: This may be no longer true, now it seems to return NoVerifiedPhone if there is no phone number. We'll see.
EResult::Fail => AccountLinkError::GenericFailure,
r => AccountLinkError::UnknownEResult(r),
}
}
}
#[derive(Error, Debug)]
pub enum FinalizeLinkError {
#[error("Provided SMS code was incorrect.")]
BadSmsCode,
/// Steam wants more 2fa codes to verify that we can generate valid codes. Call finalize again.
#[error("Steam wants more 2fa codes for verification.")]
WantMore { server_time: u64 },
#[error("Steam returned an unexpected error code: {0:?}")]
UnknownEResult(EResult),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}
impl From<EResult> for FinalizeLinkError {
fn from(result: EResult) -> Self {
match result {
EResult::TwoFactorActivationCodeMismatch => FinalizeLinkError::BadSmsCode,
r => FinalizeLinkError::UnknownEResult(r),
}
}
}