2023-06-22 22:20:15 +02:00
use crate ::protobufs ::service_twofactor ::{
CTwoFactor_AddAuthenticator_Request , CTwoFactor_FinalizeAddAuthenticator_Request ,
} ;
use crate ::steamapi ::twofactor ::TwoFactorClient ;
use crate ::token ::TwoFactorSecret ;
2023-07-02 13:17:09 +02:00
use crate ::transport ::Transport ;
2023-06-23 19:36:23 +02:00
use crate ::{ steamapi ::EResult , token ::Tokens , SteamGuardAccount } ;
2021-08-09 06:09:34 +02:00
use log ::* ;
2021-08-10 00:44:42 +02:00
use thiserror ::Error ;
2021-07-27 22:24:56 +02:00
2021-08-09 00:32:50 +02:00
#[ derive(Debug) ]
2023-07-02 13:17:09 +02:00
pub struct AccountLinker < T >
where
T : Transport ,
{
2021-08-08 18:54:46 +02:00
device_id : String ,
2021-08-09 00:32:50 +02:00
pub account : Option < SteamGuardAccount > ,
2021-08-05 03:26:14 +02:00
pub finalized : bool ,
2023-06-22 22:20:15 +02:00
tokens : Tokens ,
2023-07-02 13:17:09 +02:00
client : TwoFactorClient < T > ,
2021-07-27 22:24:56 +02:00
}
2023-07-02 13:17:09 +02:00
impl < T > AccountLinker < T >
where
T : Transport ,
{
pub fn new ( transport : T , tokens : Tokens ) -> Self {
2023-06-22 22:20:15 +02:00
Self {
2021-08-08 18:54:46 +02:00
device_id : generate_device_id ( ) ,
2021-08-09 00:32:50 +02:00
account : None ,
2021-08-05 03:26:14 +02:00
finalized : false ,
2023-06-22 22:20:15 +02:00
tokens ,
2023-07-02 13:17:09 +02:00
client : TwoFactorClient ::new ( transport ) ,
2023-06-22 22:20:15 +02:00
}
2021-08-08 18:54:46 +02:00
}
2021-07-27 22:24:56 +02:00
2023-06-25 19:11:24 +02:00
pub fn tokens ( & self ) -> & Tokens {
& self . tokens
}
2021-08-05 03:26:14 +02:00
2023-06-25 19:11:24 +02:00
pub fn link ( & mut self ) -> anyhow ::Result < AccountLinkSuccess , AccountLinkError > {
2023-06-22 22:20:15 +02:00
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 ( ) ) ;
2021-08-09 00:32:50 +02:00
}
2023-06-22 22:20:15 +02:00
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 ( ) ) ,
2023-06-23 19:36:23 +02:00
token_gid : resp . take_token_gid ( ) ,
identity_secret : base64 ::encode ( resp . take_identity_secret ( ) ) . into ( ) ,
2023-06-22 22:20:15 +02:00
device_id : self . device_id . clone ( ) ,
2023-06-23 19:36:23 +02:00
secret_1 : base64 ::encode ( resp . take_secret_1 ( ) ) . into ( ) ,
2023-06-22 22:20:15 +02:00
tokens : Some ( self . tokens . clone ( ) ) ,
} ;
let success = AccountLinkSuccess {
2023-06-23 19:36:23 +02:00
account ,
2023-06-22 22:20:15 +02:00
server_time : resp . server_time ( ) ,
phone_number_hint : resp . take_phone_number_hint ( ) ,
} ;
Ok ( success )
2021-08-08 18:54:46 +02:00
}
2021-07-27 22:24:56 +02:00
2021-08-09 06:09:34 +02:00
/// 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 ,
2023-06-22 22:20:15 +02:00
time : u64 ,
2021-08-09 06:09:34 +02:00
account : & mut SteamGuardAccount ,
sms_code : String ,
2021-08-10 00:44:42 +02:00
) -> anyhow ::Result < ( ) , FinalizeLinkError > {
2021-08-09 06:09:34 +02:00
let code = account . generate_code ( time ) ;
2023-06-22 22:20:15 +02:00
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 ( ) ) ;
2021-08-09 06:09:34 +02:00
}
2023-06-22 22:20:15 +02:00
let resp = resp . into_response_data ( ) ;
if resp . want_more ( ) {
return Err ( FinalizeLinkError ::WantMore {
server_time : resp . server_time ( ) ,
} ) ;
2021-08-09 06:09:34 +02:00
}
self . finalized = true ;
2023-06-23 19:36:23 +02:00
Ok ( ( ) )
2021-08-09 06:09:34 +02:00
}
2021-07-27 22:24:56 +02:00
}
2023-06-22 22:20:15 +02:00
#[ 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
}
}
2021-07-27 22:24:56 +02:00
fn generate_device_id ( ) -> String {
2023-06-23 19:36:23 +02:00
format! ( " android: {} " , uuid ::Uuid ::new_v4 ( ) )
2021-07-27 22:24:56 +02:00
}
2021-08-10 00:44:42 +02:00
#[ derive(Error, Debug) ]
2021-08-05 03:26:14 +02:00
pub enum AccountLinkError {
/// No phone number on the account
2021-08-10 00:44:42 +02:00
#[ error( " A phone number is needed, but not already present on the account. " ) ]
2021-08-05 03:26:14 +02:00
MustProvidePhoneNumber ,
/// User need to click link from confirmation email
2021-08-10 00:44:42 +02:00
#[ error( " An email has been sent to the user's email, click the link in that email. " ) ]
2021-08-05 03:26:14 +02:00
MustConfirmEmail ,
2023-06-25 19:11:24 +02:00
#[ error( " Authenticator is already present on this account. " ) ]
2021-08-05 03:26:14 +02:00
AuthenticatorPresent ,
2023-06-25 19:11:24 +02:00
#[ error( " You are sending too many requests to Steam, and we got rate limited. Wait at least a couple hours and try again. " ) ]
RateLimitExceeded ,
2022-12-05 16:36:38 +01:00
#[ 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. " ) ]
2022-01-02 15:46:01 +01:00
GenericFailure ,
2023-06-22 22:20:15 +02:00
#[ error( " Steam returned an unexpected error code: {0:?} " ) ]
UnknownEResult ( EResult ) ,
2021-08-10 00:44:42 +02:00
#[ error(transparent) ]
Unknown ( #[ from ] anyhow ::Error ) ,
2021-08-05 03:26:14 +02:00
}
2023-06-22 22:20:15 +02:00
impl From < EResult > for AccountLinkError {
fn from ( result : EResult ) -> Self {
match result {
2023-06-25 19:11:24 +02:00
EResult ::RateLimitExceeded = > AccountLinkError ::RateLimitExceeded ,
EResult ::NoVerifiedPhone = > AccountLinkError ::MustProvidePhoneNumber ,
2023-06-22 22:20:15 +02:00
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.
2023-06-25 19:11:24 +02:00
// update 2023: This may be no longer true, now it seems to return NoVerifiedPhone if there is no phone number. We'll see.
2023-06-22 22:20:15 +02:00
EResult ::Fail = > AccountLinkError ::GenericFailure ,
r = > AccountLinkError ::UnknownEResult ( r ) ,
}
}
}
2021-08-10 00:44:42 +02:00
#[ derive(Error, Debug) ]
2021-08-09 06:09:34 +02:00
pub enum FinalizeLinkError {
2021-08-10 00:44:42 +02:00
#[ error( " Provided SMS code was incorrect. " ) ]
2021-08-09 06:09:34 +02:00
BadSmsCode ,
/// Steam wants more 2fa codes to verify that we can generate valid codes. Call finalize again.
2021-08-10 00:44:42 +02:00
#[ error( " Steam wants more 2fa codes for verification. " ) ]
2023-06-22 22:20:15 +02:00
WantMore { server_time : u64 } ,
#[ error( " Steam returned an unexpected error code: {0:?} " ) ]
UnknownEResult ( EResult ) ,
2021-08-10 00:44:42 +02:00
#[ error(transparent) ]
Unknown ( #[ from ] anyhow ::Error ) ,
2021-08-05 03:26:14 +02:00
}
2023-06-22 22:20:15 +02:00
impl From < EResult > for FinalizeLinkError {
fn from ( result : EResult ) -> Self {
match result {
EResult ::TwoFactorActivationCodeMismatch = > FinalizeLinkError ::BadSmsCode ,
r = > FinalizeLinkError ::UnknownEResult ( r ) ,
}
}
}