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
This commit is contained in:
Carson McManus 2023-06-25 13:11:24 -04:00 committed by GitHub
parent d87caa06f6
commit 1632e2f10e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 530 additions and 53 deletions

116
Cargo.lock generated
View file

@ -22,9 +22,9 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.18" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -92,6 +92,15 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c" checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.2" version = "0.5.2"
@ -848,6 +857,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.8" version = "0.4.8"
@ -896,6 +914,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.7" version = "0.4.7"
@ -915,6 +939,15 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@ -969,6 +1002,12 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.5.3" version = "0.5.3"
@ -1002,6 +1041,16 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.7.0" version = "0.7.0"
@ -1084,6 +1133,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
[[package]]
name = "oncemutex"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.0" version = "0.3.0"
@ -1213,6 +1268,26 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "phonenumber"
version = "0.3.2+8.13.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34749f64ea9d76f10cdc8a859588b57775f59177c7dd91f744d620bd62982d6f"
dependencies = [
"bincode",
"either",
"fnv",
"itertools",
"lazy_static 1.4.0",
"nom",
"quick-xml",
"regex",
"regex-cache",
"serde",
"serde_derive",
"thiserror",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@ -1315,7 +1390,7 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"rand_chacha 0.3.1", "rand_chacha 0.3.1",
"rand_xorshift", "rand_xorshift",
"regex-syntax", "regex-syntax 0.6.26",
"rusty-fork", "rusty-fork",
"tempfile", "tempfile",
] ]
@ -1422,6 +1497,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.18"
@ -1589,13 +1673,25 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.6" version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax", "regex-syntax 0.7.2",
]
[[package]]
name = "regex-cache"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7b62d69743b8b94f353b6b7c3deb4c5582828328bcb8d5fedf214373808793"
dependencies = [
"lru-cache",
"oncemutex",
"regex",
"regex-syntax 0.6.26",
] ]
[[package]] [[package]]
@ -1604,6 +1700,12 @@ version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]]
name = "regex-syntax"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@ -2087,6 +2189,7 @@ dependencies = [
"lazy_static 1.4.0", "lazy_static 1.4.0",
"log", "log",
"maplit", "maplit",
"phonenumber",
"protobuf", "protobuf",
"protobuf-codegen", "protobuf-codegen",
"protobuf-json-mapping", "protobuf-json-mapping",
@ -2121,6 +2224,7 @@ dependencies = [
"hmac-sha1", "hmac-sha1",
"lazy_static 1.4.0", "lazy_static 1.4.0",
"log", "log",
"phonenumber",
"proptest", "proptest",
"qrcode", "qrcode",
"rand 0.8.5", "rand 0.8.5",

View file

@ -61,6 +61,7 @@ secrecy = { version = "0.8", features = ["serde"] }
zeroize = "^1.4.3" zeroize = "^1.4.3"
serde_path_to_error = "0.1.11" serde_path_to_error = "0.1.11"
update-informer = { version = "1.0.0", optional = true, default-features = false, features = ["github"] } update-informer = { version = "1.0.0", optional = true, default-features = false, features = ["github"] }
phonenumber = "0.3"
[dev-dependencies] [dev-dependencies]
tempdir = "0.3" tempdir = "0.3"

View file

@ -1,7 +1,9 @@
use log::*; use log::*;
use phonenumber::PhoneNumber;
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
use steamguard::{ use steamguard::{
accountlinker::AccountLinkSuccess, AccountLinkError, AccountLinker, FinalizeLinkError, accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient,
token::Tokens, transport::WebApiTransport, AccountLinkError, AccountLinker, FinalizeLinkError,
}; };
use crate::{tui, AccountManager}; use crate::{tui, AccountManager};
@ -25,11 +27,11 @@ impl ManifestCommand for SetupCommand {
); );
} }
info!("Logging in to {}", username); info!("Logging in to {}", username);
let session = let tokens =
crate::do_login_raw(username).expect("Failed to log in. Account has not been linked."); crate::do_login_raw(username).expect("Failed to log in. Account has not been linked.");
info!("Adding authenticator..."); info!("Adding authenticator...");
let mut linker = AccountLinker::new(session); let mut linker = AccountLinker::new(tokens);
let link: AccountLinkSuccess; let link: AccountLinkSuccess;
loop { loop {
match linker.link() { match linker.link() {
@ -37,18 +39,9 @@ impl ManifestCommand for SetupCommand {
link = a; link = a;
break; break;
} }
Err(AccountLinkError::MustRemovePhoneNumber) => {
println!("There is already a phone number on this account, please remove it and try again.");
bail!("There is already a phone number on this account, please remove it and try again.");
}
Err(AccountLinkError::MustProvidePhoneNumber) => { Err(AccountLinkError::MustProvidePhoneNumber) => {
println!("Enter your phone number in the following format: +1 123-456-7890"); eprintln!("Looks like you don't have a phone number on this account.");
print!("Phone number: "); do_add_phone_number(linker.tokens())?;
linker.phone_number = tui::prompt().replace(&['(', ')', '-'][..], "");
}
Err(AccountLinkError::AuthenticatorPresent) => {
println!("An authenticator is already present on this account.");
bail!("An authenticator is already present on this account.");
} }
Err(AccountLinkError::MustConfirmEmail) => { Err(AccountLinkError::MustConfirmEmail) => {
println!("Check your email and click the link."); println!("Check your email and click the link.");
@ -70,7 +63,7 @@ impl ManifestCommand for SetupCommand {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err);
println!( eprintln!(
"Just in case, here is the account info. Save it somewhere just in case!\n{:#?}", "Just in case, here is the account info. Save it somewhere just in case!\n{:#?}",
manager.get_account(&account_name).unwrap().lock().unwrap() manager.get_account(&account_name).unwrap().lock().unwrap()
); );
@ -83,7 +76,7 @@ impl ManifestCommand for SetupCommand {
.expect("account was not present in manifest"); .expect("account was not present in manifest");
let mut account = account_arc.lock().unwrap(); let mut account = account_arc.lock().unwrap();
println!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code.expose_secret()); eprintln!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code.expose_secret());
tui::pause(); tui::pause();
debug!("attempting link finalization"); debug!("attempting link finalization");
@ -115,11 +108,11 @@ impl ManifestCommand for SetupCommand {
let revocation_code = account.revocation_code.clone(); let revocation_code = account.revocation_code.clone();
drop(account); // explicitly drop the lock so we don't hang on the mutex drop(account); // explicitly drop the lock so we don't hang on the mutex
println!("Authenticator finalized."); info!("Authenticator finalized.");
match manager.save() { match manager.save() {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
println!( error!(
"Failed to save manifest, but we were able to save it before. {}", "Failed to save manifest, but we were able to save it before. {}",
err err
); );
@ -127,7 +120,7 @@ impl ManifestCommand for SetupCommand {
} }
} }
println!( eprintln!(
"Authenticator has been finalized. Please actually write down your revocation code: {}", "Authenticator has been finalized. Please actually write down your revocation code: {}",
revocation_code.expose_secret() revocation_code.expose_secret()
); );
@ -135,3 +128,52 @@ impl ManifestCommand for SetupCommand {
Ok(()) Ok(())
} }
} }
pub fn do_add_phone_number(tokens: &Tokens) -> anyhow::Result<()> {
let client = PhoneClient::new(WebApiTransport::new());
let linker = PhoneLinker::new(client, tokens.clone());
let phone_number: PhoneNumber;
loop {
eprintln!("Enter your phone number, including country code, in this format: +1 1234567890");
eprint!("Phone number: ");
let number = tui::prompt();
match phonenumber::parse(None, &number) {
Ok(p) => {
phone_number = p;
break;
}
Err(err) => {
error!("Failed to parse phone number: {}", err);
}
}
}
let resp = linker.set_account_phone_number(phone_number)?;
eprintln!(
"Please click the link in the email sent to {}",
resp.confirmation_email_address()
);
tui::pause();
debug!("sending phone verification code");
linker.send_phone_verification_code(0)?;
loop {
eprint!("Enter the code sent to your phone: ");
let code = tui::prompt();
match linker.verify_account_phone_with_code(code) {
Ok(_) => break,
Err(err) => {
error!("Failed to verify phone number: {}", err);
}
}
}
info!("Successfully added phone number to account");
Ok(())
}

View file

@ -33,6 +33,7 @@ zeroize = "^1.4.3"
protobuf = "3.2.0" protobuf = "3.2.0"
protobuf-json-mapping = "3.2.0" protobuf-json-mapping = "3.2.0"
hmac-sha256 = "1.1.7" hmac-sha256 = "1.1.7"
phonenumber = "0.3"
[build-dependencies] [build-dependencies]
anyhow = "^1.0" anyhow = "^1.0"

View file

@ -0,0 +1,50 @@
message CPhone_AddPhoneToAccount_Response {
optional bool success = 1;
optional int32 phone_number_type = 2;
}
message CPhone_ConfirmAddPhoneToAccount_Request {
optional fixed64 steamid = 1;
optional string stoken = 2;
}
message CPhone_IsAccountWaitingForEmailConfirmation_Request {
}
message CPhone_IsAccountWaitingForEmailConfirmation_Response {
optional bool awaiting_email_confirmation = 1;
optional uint32 seconds_to_wait = 2;
}
message CPhone_SendPhoneVerificationCode_Request {
optional uint32 language = 1;
}
message CPhone_SendPhoneVerificationCode_Response {
}
message CPhone_SetAccountPhoneNumber_Request {
optional string phone_number = 1;
optional string phone_country_code = 2;
}
message CPhone_SetAccountPhoneNumber_Response {
optional string confirmation_email_address = 1;
optional string phone_number_formatted = 2;
}
message CPhone_VerifyAccountPhoneWithCode_Request {
optional string code = 1;
}
message CPhone_VerifyAccountPhoneWithCode_Response {
}
service Phone {
rpc ConfirmAddPhoneToAccount (.CPhone_ConfirmAddPhoneToAccount_Request) returns (.CPhone_AddPhoneToAccount_Response);
rpc IsAccountWaitingForEmailConfirmation (.CPhone_IsAccountWaitingForEmailConfirmation_Request) returns (.CPhone_IsAccountWaitingForEmailConfirmation_Response);
rpc SendPhoneVerificationCode (.CPhone_SendPhoneVerificationCode_Request) returns (.CPhone_SendPhoneVerificationCode_Response);
rpc SetAccountPhoneNumber (.CPhone_SetAccountPhoneNumber_Request) returns (.CPhone_SetAccountPhoneNumber_Response);
rpc VerifyAccountPhoneWithCode (.CPhone_VerifyAccountPhoneWithCode_Request) returns (.CPhone_VerifyAccountPhoneWithCode_Response);
}

View file

@ -11,7 +11,6 @@ use thiserror::Error;
#[derive(Debug)] #[derive(Debug)]
pub struct AccountLinker { pub struct AccountLinker {
device_id: String, device_id: String,
pub phone_number: String,
pub account: Option<SteamGuardAccount>, pub account: Option<SteamGuardAccount>,
pub finalized: bool, pub finalized: bool,
tokens: Tokens, tokens: Tokens,
@ -22,7 +21,6 @@ impl AccountLinker {
pub fn new(tokens: Tokens) -> AccountLinker { pub fn new(tokens: Tokens) -> AccountLinker {
Self { Self {
device_id: generate_device_id(), device_id: generate_device_id(),
phone_number: "".into(),
account: None, account: None,
finalized: false, finalized: false,
tokens, tokens,
@ -30,29 +28,11 @@ impl AccountLinker {
} }
} }
pub fn tokens(&self) -> &Tokens {
&self.tokens
}
pub fn link(&mut self) -> anyhow::Result<AccountLinkSuccess, AccountLinkError> { pub fn link(&mut self) -> anyhow::Result<AccountLinkSuccess, AccountLinkError> {
// let has_phone = self.client.has_phone()?;
// if has_phone && !self.phone_number.is_empty() {
// return Err(AccountLinkError::MustRemovePhoneNumber);
// }
// if !has_phone && self.phone_number.is_empty() {
// return Err(AccountLinkError::MustProvidePhoneNumber);
// }
// if !has_phone {
// if self.sent_confirmation_email {
// if !self.client.check_email_confirmation()? {
// return Err(anyhow!("Failed email confirmation check"))?;
// }
// } else if !self.client.add_phone_number(self.phone_number.clone())? {
// return Err(anyhow!("Failed to add phone number"))?;
// } else {
// self.sent_confirmation_email = true;
// return Err(AccountLinkError::MustConfirmEmail);
// }
// }
let access_token = self.tokens.access_token(); let access_token = self.tokens.access_token();
let steam_id = access_token.decode()?.steam_id(); let steam_id = access_token.decode()?.steam_id();
@ -162,14 +142,13 @@ pub enum AccountLinkError {
/// No phone number on the account /// No phone number on the account
#[error("A phone number is needed, but not already present on the account.")] #[error("A phone number is needed, but not already present on the account.")]
MustProvidePhoneNumber, MustProvidePhoneNumber,
/// A phone number is already on the account
#[error("A phone number was provided, but one is already present on the account.")]
MustRemovePhoneNumber,
/// User need to click link from confirmation email /// 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.")] #[error("An email has been sent to the user's email, click the link in that email.")]
MustConfirmEmail, MustConfirmEmail,
#[error("Authenticator is already present.")] #[error("Authenticator is already present on this account.")]
AuthenticatorPresent, 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.")] #[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, GenericFailure,
#[error("Steam returned an unexpected error code: {0:?}")] #[error("Steam returned an unexpected error code: {0:?}")]
@ -181,10 +160,13 @@ pub enum AccountLinkError {
impl From<EResult> for AccountLinkError { impl From<EResult> for AccountLinkError {
fn from(result: EResult) -> Self { fn from(result: EResult) -> Self {
match result { match result {
EResult::RateLimitExceeded => AccountLinkError::RateLimitExceeded,
EResult::NoVerifiedPhone => AccountLinkError::MustProvidePhoneNumber,
EResult::DuplicateRequest => AccountLinkError::AuthenticatorPresent, EResult::DuplicateRequest => AccountLinkError::AuthenticatorPresent,
// If the user has no phone number on their account, it will always return this status code. // 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 // 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. // 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, EResult::Fail => AccountLinkError::GenericFailure,
r => AccountLinkError::UnknownEResult(r), r => AccountLinkError::UnknownEResult(r),
} }

View file

@ -30,6 +30,7 @@ extern crate maplit;
pub mod accountlinker; pub mod accountlinker;
mod api_responses; mod api_responses;
mod confirmation; mod confirmation;
pub mod phonelinker;
pub mod protobufs; pub mod protobufs;
mod qrapprover; mod qrapprover;
pub mod refresher; pub mod refresher;

View file

@ -0,0 +1,166 @@
use crate::protobufs::service_phone::*;
use crate::transport::TransportError;
use crate::{
steamapi::{EResult, PhoneClient},
token::Tokens,
transport::WebApiTransport,
};
pub use phonenumber::PhoneNumber;
pub struct PhoneLinker {
client: PhoneClient<WebApiTransport>,
tokens: Tokens,
}
impl PhoneLinker {
pub fn new(client: PhoneClient<WebApiTransport>, tokens: Tokens) -> Self {
Self { client, tokens }
}
/// If successful, wait for the user to click the link in the email, then immediately call [`send_phone_verification_code`].
pub fn set_account_phone_number(
&self,
phone_number: PhoneNumber,
) -> Result<SetAccountPhoneNumberResponse, SetPhoneNumberError> {
// This results in an email being sent to the account's email address with a link to click on to confirm the phone number.
// This endpoint also does almost no validation of the phone number. It only validates it after the user clicks the link.
// `phone_number` needs to include the country code in the format `11234567890`
let mut req = CPhone_SetAccountPhoneNumber_Request::new();
req.set_phone_number(
phone_number
.format()
.mode(phonenumber::Mode::E164)
.to_string(),
);
req.set_phone_country_code(phone_number.code().value().to_string());
let resp = self
.client
.set_account_phone_number(req, self.tokens.access_token())?;
if resp.result != EResult::Pending {
return Err(SetPhoneNumberError::UnknownEResult(resp.result));
}
let resp = resp.into_response_data();
Ok(resp.into())
}
// confirm_add_phone_to_account is actually not needed, because it's performed by the user clicking the link in the email.
/// language 0 is english
pub fn send_phone_verification_code(&self, language: u32) -> anyhow::Result<()> {
let mut req = CPhone_SendPhoneVerificationCode_Request::new();
req.set_language(language);
let resp = self
.client
.send_phone_verification_code(req, self.tokens.access_token())?;
if resp.result != EResult::OK {
return Err(anyhow::anyhow!(
"Failed to send phone verification code: {:?}",
resp.result
));
}
Ok(())
}
pub fn verify_account_phone_with_code(
&self,
code: String,
) -> anyhow::Result<(), VerifyPhoneError> {
let mut req = CPhone_VerifyAccountPhoneWithCode_Request::new();
req.set_code(code);
let resp = self
.client
.verify_account_phone_with_code(req, self.tokens.access_token())?;
if resp.result != EResult::OK {
return Err(resp.result.into());
}
Ok(())
}
/// If true, returns `Some` with the value inside being the time in seconds until the email expires.
pub fn is_account_waiting_for_email_confirmation(&self) -> anyhow::Result<Option<u32>> {
let req = CPhone_IsAccountWaitingForEmailConfirmation_Request::new();
let resp = self
.client
.is_account_waiting_for_email_confirmation(req, self.tokens.access_token())?;
if resp.result != EResult::OK {
return Err(anyhow::anyhow!(
"Failed to check if account is waiting for email confirmation: {:?}",
resp.result
));
}
let resp = resp.into_response_data();
if !resp.awaiting_email_confirmation() {
return Ok(None);
}
Ok(resp.seconds_to_wait)
}
}
#[derive(Debug, thiserror::Error)]
pub enum SetPhoneNumberError {
#[error("Transport error: {0}")]
TransportError(#[from] TransportError),
#[error("Steam says: {0:?}")]
UnknownEResult(EResult),
}
impl From<EResult> for SetPhoneNumberError {
fn from(result: EResult) -> Self {
SetPhoneNumberError::UnknownEResult(result)
}
}
#[derive(Debug)]
pub struct SetAccountPhoneNumberResponse {
confirmation_email_address: String,
phone_number_formatted: String,
}
impl SetAccountPhoneNumberResponse {
pub fn confirmation_email_address(&self) -> &str {
&self.confirmation_email_address
}
pub fn phone_number_formatted(&self) -> &str {
&self.phone_number_formatted
}
}
impl From<CPhone_SetAccountPhoneNumber_Response> for SetAccountPhoneNumberResponse {
fn from(mut resp: CPhone_SetAccountPhoneNumber_Response) -> Self {
Self {
confirmation_email_address: resp.take_confirmation_email_address(),
phone_number_formatted: resp.take_phone_number_formatted(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum VerifyPhoneError {
#[error("Transport error: {0}")]
TransportError(#[from] TransportError),
#[error("Steam says: {0:?}")]
UnknownEResult(EResult),
}
impl From<EResult> for VerifyPhoneError {
fn from(result: EResult) -> Self {
VerifyPhoneError::UnknownEResult(result)
}
}

View file

@ -1,4 +1,5 @@
pub mod authentication; pub mod authentication;
pub mod phone;
pub mod twofactor; pub mod twofactor;
use crate::{ use crate::{
@ -8,6 +9,7 @@ use reqwest::Url;
use serde::Deserialize; use serde::Deserialize;
pub use self::authentication::AuthenticationClient; pub use self::authentication::AuthenticationClient;
pub use self::phone::PhoneClient;
pub use self::twofactor::TwoFactorClient; pub use self::twofactor::TwoFactorClient;
lazy_static! { lazy_static! {

View file

@ -0,0 +1,128 @@
use crate::{
protobufs::service_phone::*,
token::Jwt,
transport::{Transport, TransportError},
};
const SERVICE_NAME: &str = "IPhoneService";
use super::{ApiRequest, ApiResponse, BuildableRequest};
/// A client for the IPhoneService API.
#[derive(Debug)]
pub struct PhoneClient<T>
where
T: Transport,
{
transport: T,
}
impl<T> PhoneClient<T>
where
T: Transport,
{
#[must_use]
pub fn new(transport: T) -> Self {
Self { transport }
}
pub fn set_account_phone_number(
&self,
req: CPhone_SetAccountPhoneNumber_Request,
access_token: &Jwt,
) -> Result<ApiResponse<CPhone_SetAccountPhoneNumber_Response>, TransportError> {
let req = ApiRequest::new(SERVICE_NAME, "SetAccountPhoneNumber", 1u32, req)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CPhone_SetAccountPhoneNumber_Request, CPhone_SetAccountPhoneNumber_Response>(
req,
)?;
Ok(resp)
}
pub fn send_phone_verification_code(
&self,
req: CPhone_SendPhoneVerificationCode_Request,
access_token: &Jwt,
) -> Result<ApiResponse<CPhone_SendPhoneVerificationCode_Response>, TransportError> {
let req = ApiRequest::new(SERVICE_NAME, "SendPhoneVerificationCode", 1u32, req)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CPhone_SendPhoneVerificationCode_Request, CPhone_SendPhoneVerificationCode_Response>(
req,
)?;
Ok(resp)
}
pub fn is_account_waiting_for_email_confirmation(
&self,
req: CPhone_IsAccountWaitingForEmailConfirmation_Request,
access_token: &Jwt,
) -> Result<ApiResponse<CPhone_IsAccountWaitingForEmailConfirmation_Response>, TransportError> {
let req = ApiRequest::new(
SERVICE_NAME,
"IsAccountWaitingForEmailConfirmation",
1u32,
req,
)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CPhone_IsAccountWaitingForEmailConfirmation_Request, CPhone_IsAccountWaitingForEmailConfirmation_Response>(
req,
)?;
Ok(resp)
}
pub fn confirm_add_phone_to_account(
&self,
req: CPhone_ConfirmAddPhoneToAccount_Request,
access_token: &Jwt,
) -> Result<ApiResponse<CPhone_AddPhoneToAccount_Response>, TransportError> {
let req = ApiRequest::new(SERVICE_NAME, "ConfirmAddPhoneToAccount", 1u32, req)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CPhone_ConfirmAddPhoneToAccount_Request, CPhone_AddPhoneToAccount_Response>(
req,
)?;
Ok(resp)
}
pub fn verify_account_phone_with_code(
&self,
req: CPhone_VerifyAccountPhoneWithCode_Request,
access_token: &Jwt,
) -> Result<ApiResponse<CPhone_VerifyAccountPhoneWithCode_Response>, TransportError> {
let req = ApiRequest::new(SERVICE_NAME, "VerifyAccountPhoneWithCode", 1u32, req)
.with_access_token(access_token);
let resp = self
.transport
.send_request::<CPhone_VerifyAccountPhoneWithCode_Request, CPhone_VerifyAccountPhoneWithCode_Response>(
req,
)?;
Ok(resp)
}
}
macro_rules! impl_buildable_req {
($type:ty, $needs_auth:literal) => {
impl BuildableRequest for $type {
fn method() -> reqwest::Method {
reqwest::Method::POST
}
fn requires_access_token() -> bool {
$needs_auth
}
}
};
}
impl_buildable_req!(CPhone_SetAccountPhoneNumber_Request, true);
impl_buildable_req!(CPhone_SendPhoneVerificationCode_Request, true);
impl_buildable_req!(CPhone_IsAccountWaitingForEmailConfirmation_Request, true);
impl_buildable_req!(CPhone_ConfirmAddPhoneToAccount_Request, true);
impl_buildable_req!(CPhone_VerifyAccountPhoneWithCode_Request, true);