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:
parent
d87caa06f6
commit
1632e2f10e
10 changed files with 530 additions and 53 deletions
116
Cargo.lock
generated
116
Cargo.lock
generated
|
@ -22,9 +22,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -92,6 +92,15 @@ version = "1.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.2"
|
||||
|
@ -848,6 +857,15 @@ version = "2.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
|
@ -896,6 +914,12 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
|
@ -915,6 +939,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
|
@ -969,6 +1002,12 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.3"
|
||||
|
@ -1002,6 +1041,16 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.7.0"
|
||||
|
@ -1084,6 +1133,12 @@ version = "1.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||
|
||||
[[package]]
|
||||
name = "oncemutex"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.0"
|
||||
|
@ -1213,6 +1268,26 @@ dependencies = [
|
|||
"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]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
|
@ -1315,7 +1390,7 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.6.26",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
]
|
||||
|
@ -1422,6 +1497,15 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "quote"
|
||||
version = "1.0.18"
|
||||
|
@ -1589,13 +1673,25 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.6"
|
||||
version = "1.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
|
||||
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"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]]
|
||||
|
@ -1604,6 +1700,12 @@ version = "0.6.26"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
|
@ -2087,6 +2189,7 @@ dependencies = [
|
|||
"lazy_static 1.4.0",
|
||||
"log",
|
||||
"maplit",
|
||||
"phonenumber",
|
||||
"protobuf",
|
||||
"protobuf-codegen",
|
||||
"protobuf-json-mapping",
|
||||
|
@ -2121,6 +2224,7 @@ dependencies = [
|
|||
"hmac-sha1",
|
||||
"lazy_static 1.4.0",
|
||||
"log",
|
||||
"phonenumber",
|
||||
"proptest",
|
||||
"qrcode",
|
||||
"rand 0.8.5",
|
||||
|
|
|
@ -61,6 +61,7 @@ secrecy = { version = "0.8", features = ["serde"] }
|
|||
zeroize = "^1.4.3"
|
||||
serde_path_to_error = "0.1.11"
|
||||
update-informer = { version = "1.0.0", optional = true, default-features = false, features = ["github"] }
|
||||
phonenumber = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3"
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use log::*;
|
||||
use phonenumber::PhoneNumber;
|
||||
use secrecy::ExposeSecret;
|
||||
use steamguard::{
|
||||
accountlinker::AccountLinkSuccess, AccountLinkError, AccountLinker, FinalizeLinkError,
|
||||
accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient,
|
||||
token::Tokens, transport::WebApiTransport, AccountLinkError, AccountLinker, FinalizeLinkError,
|
||||
};
|
||||
|
||||
use crate::{tui, AccountManager};
|
||||
|
@ -25,11 +27,11 @@ impl ManifestCommand for SetupCommand {
|
|||
);
|
||||
}
|
||||
info!("Logging in to {}", username);
|
||||
let session =
|
||||
let tokens =
|
||||
crate::do_login_raw(username).expect("Failed to log in. Account has not been linked.");
|
||||
|
||||
info!("Adding authenticator...");
|
||||
let mut linker = AccountLinker::new(session);
|
||||
let mut linker = AccountLinker::new(tokens);
|
||||
let link: AccountLinkSuccess;
|
||||
loop {
|
||||
match linker.link() {
|
||||
|
@ -37,18 +39,9 @@ impl ManifestCommand for SetupCommand {
|
|||
link = a;
|
||||
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) => {
|
||||
println!("Enter your phone number in the following format: +1 123-456-7890");
|
||||
print!("Phone number: ");
|
||||
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.");
|
||||
eprintln!("Looks like you don't have a phone number on this account.");
|
||||
do_add_phone_number(linker.tokens())?;
|
||||
}
|
||||
Err(AccountLinkError::MustConfirmEmail) => {
|
||||
println!("Check your email and click the link.");
|
||||
|
@ -70,7 +63,7 @@ impl ManifestCommand for SetupCommand {
|
|||
Ok(_) => {}
|
||||
Err(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{:#?}",
|
||||
manager.get_account(&account_name).unwrap().lock().unwrap()
|
||||
);
|
||||
|
@ -83,7 +76,7 @@ impl ManifestCommand for SetupCommand {
|
|||
.expect("account was not present in manifest");
|
||||
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();
|
||||
|
||||
debug!("attempting link finalization");
|
||||
|
@ -115,11 +108,11 @@ impl ManifestCommand for SetupCommand {
|
|||
let revocation_code = account.revocation_code.clone();
|
||||
drop(account); // explicitly drop the lock so we don't hang on the mutex
|
||||
|
||||
println!("Authenticator finalized.");
|
||||
info!("Authenticator finalized.");
|
||||
match manager.save() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
println!(
|
||||
error!(
|
||||
"Failed to save manifest, but we were able to save it before. {}",
|
||||
err
|
||||
);
|
||||
|
@ -127,7 +120,7 @@ impl ManifestCommand for SetupCommand {
|
|||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
eprintln!(
|
||||
"Authenticator has been finalized. Please actually write down your revocation code: {}",
|
||||
revocation_code.expose_secret()
|
||||
);
|
||||
|
@ -135,3 +128,52 @@ impl ManifestCommand for SetupCommand {
|
|||
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(())
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ zeroize = "^1.4.3"
|
|||
protobuf = "3.2.0"
|
||||
protobuf-json-mapping = "3.2.0"
|
||||
hmac-sha256 = "1.1.7"
|
||||
phonenumber = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "^1.0"
|
||||
|
|
50
steamguard/protobufs/service_phone.proto
Normal file
50
steamguard/protobufs/service_phone.proto
Normal 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);
|
||||
}
|
|
@ -11,7 +11,6 @@ use thiserror::Error;
|
|||
#[derive(Debug)]
|
||||
pub struct AccountLinker {
|
||||
device_id: String,
|
||||
pub phone_number: String,
|
||||
pub account: Option<SteamGuardAccount>,
|
||||
pub finalized: bool,
|
||||
tokens: Tokens,
|
||||
|
@ -22,7 +21,6 @@ impl AccountLinker {
|
|||
pub fn new(tokens: Tokens) -> AccountLinker {
|
||||
Self {
|
||||
device_id: generate_device_id(),
|
||||
phone_number: "".into(),
|
||||
account: None,
|
||||
finalized: false,
|
||||
tokens,
|
||||
|
@ -30,29 +28,11 @@ impl AccountLinker {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn tokens(&self) -> &Tokens {
|
||||
&self.tokens
|
||||
}
|
||||
|
||||
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 steam_id = access_token.decode()?.steam_id();
|
||||
|
||||
|
@ -162,14 +142,13 @@ pub enum AccountLinkError {
|
|||
/// No phone number on the account
|
||||
#[error("A phone number is needed, but not already present on the account.")]
|
||||
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
|
||||
#[error("An email has been sent to the user's email, click the link in that email.")]
|
||||
MustConfirmEmail,
|
||||
#[error("Authenticator is already present.")]
|
||||
#[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:?}")]
|
||||
|
@ -181,10 +160,13 @@ pub enum AccountLinkError {
|
|||
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),
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ extern crate maplit;
|
|||
pub mod accountlinker;
|
||||
mod api_responses;
|
||||
mod confirmation;
|
||||
pub mod phonelinker;
|
||||
pub mod protobufs;
|
||||
mod qrapprover;
|
||||
pub mod refresher;
|
||||
|
|
166
steamguard/src/phonelinker.rs
Normal file
166
steamguard/src/phonelinker.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod authentication;
|
||||
pub mod phone;
|
||||
pub mod twofactor;
|
||||
|
||||
use crate::{
|
||||
|
@ -8,6 +9,7 @@ use reqwest::Url;
|
|||
use serde::Deserialize;
|
||||
|
||||
pub use self::authentication::AuthenticationClient;
|
||||
pub use self::phone::PhoneClient;
|
||||
pub use self::twofactor::TwoFactorClient;
|
||||
|
||||
lazy_static! {
|
||||
|
|
128
steamguard/src/steamapi/phone.rs
Normal file
128
steamguard/src/steamapi/phone.rs
Normal 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);
|
Loading…
Reference in a new issue