reorganize steam api responses

This commit is contained in:
Carson McManus 2022-12-06 10:02:07 -05:00
parent 911ead0242
commit 99f977d7df
8 changed files with 310 additions and 283 deletions

View file

@ -1,7 +1,6 @@
use crate::{
steamapi::{
AddAuthenticatorResponse, FinalizeAddAuthenticatorResponse, Session, SteamApiClient,
},
api_responses::{AddAuthenticatorResponse, FinalizeAddAuthenticatorResponse},
steamapi::{Session, SteamApiClient},
SteamGuardAccount,
};
use log::*;

View file

@ -0,0 +1,130 @@
use crate::{token::TwoFactorSecret, SteamGuardAccount};
use super::parse_json_string_as_number;
use serde::{Deserialize, Serialize};
/// Represents the response from `/ITwoFactorService/QueryTime/v0001`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryTimeResponse {
/// The time that the server will use to check your two factor code.
#[serde(deserialize_with = "parse_json_string_as_number")]
pub server_time: u64,
#[serde(deserialize_with = "parse_json_string_as_number")]
pub skew_tolerance_seconds: u64,
#[serde(deserialize_with = "parse_json_string_as_number")]
pub large_time_jink: u64,
pub probe_frequency_seconds: u64,
pub adjusted_time_probe_frequency_seconds: u64,
pub hint_probe_frequency_seconds: u64,
pub sync_timeout: u64,
pub try_again_seconds: u64,
pub max_attempts: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AddAuthenticatorResponse {
/// Shared secret between server and authenticator
#[serde(default)]
pub shared_secret: String,
/// Authenticator serial number (unique per token)
#[serde(default)]
pub serial_number: String,
/// code used to revoke authenticator
#[serde(default)]
pub revocation_code: String,
/// URI for QR code generation
#[serde(default)]
pub uri: String,
/// Current server time
#[serde(default, deserialize_with = "parse_json_string_as_number")]
pub server_time: u64,
/// Account name to display on token client
#[serde(default)]
pub account_name: String,
/// Token GID assigned by server
#[serde(default)]
pub token_gid: String,
/// Secret used for identity attestation (e.g., for eventing)
#[serde(default)]
pub identity_secret: String,
/// Spare shared secret
#[serde(default)]
pub secret_1: String,
/// Result code
pub status: i32,
#[serde(default)]
pub phone_number_hint: Option<String>,
}
impl AddAuthenticatorResponse {
pub fn to_steam_guard_account(self) -> SteamGuardAccount {
SteamGuardAccount {
shared_secret: TwoFactorSecret::parse_shared_secret(self.shared_secret).unwrap(),
serial_number: self.serial_number.clone(),
revocation_code: self.revocation_code.into(),
uri: self.uri.into(),
server_time: self.server_time,
account_name: self.account_name.clone(),
token_gid: self.token_gid.clone(),
identity_secret: self.identity_secret.into(),
secret_1: self.secret_1.into(),
fully_enrolled: false,
device_id: "".into(),
session: None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct FinalizeAddAuthenticatorResponse {
pub status: i32,
#[serde(deserialize_with = "parse_json_string_as_number")]
pub server_time: u64,
pub want_more: bool,
pub success: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RemoveAuthenticatorResponse {
pub success: bool,
}
#[cfg(test)]
mod test {
use super::*;
use crate::api_responses::SteamApiResponse;
#[test]
fn test_parse_add_auth_response() {
let result = serde_json::from_str::<SteamApiResponse<AddAuthenticatorResponse>>(
include_str!("../fixtures/api-responses/add-authenticator-1.json"),
);
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap().response;
assert_eq!(resp.server_time, 1628559846);
assert_eq!(resp.shared_secret, "wGwZx=sX5MmTxi6QgA3Gi");
assert_eq!(resp.revocation_code, "R123456");
}
#[test]
fn test_parse_add_auth_response2() {
let result = serde_json::from_str::<SteamApiResponse<AddAuthenticatorResponse>>(
include_str!("../fixtures/api-responses/add-authenticator-2.json"),
);
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap().response;
assert_eq!(resp.status, 29);
}
}

View file

@ -0,0 +1,137 @@
use serde::{Deserialize, Deserializer, Serialize};
use super::parse_json_string_as_number;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginTransferParameters {
pub steamid: String,
pub token_secure: String,
pub auth: String,
pub remember_login: bool,
pub webcookie: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OAuthData {
pub oauth_token: String,
pub steamid: String,
pub wgtoken: String,
pub wgtoken_secure: String,
#[serde(default)]
pub webcookie: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RsaResponse {
pub success: bool,
pub publickey_exp: String,
pub publickey_mod: String,
pub timestamp: String,
pub token_gid: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LoginResponse {
pub success: bool,
#[serde(default)]
pub login_complete: bool,
#[serde(default)]
pub captcha_needed: bool,
#[serde(default)]
pub captcha_gid: String,
#[serde(default, deserialize_with = "parse_json_string_as_number")]
pub emailsteamid: u64,
#[serde(default)]
pub emailauth_needed: bool,
#[serde(default)]
pub requires_twofactor: bool,
#[serde(default)]
pub message: String,
#[serde(default, deserialize_with = "oauth_data_from_string")]
pub oauth: Option<OAuthData>,
pub transfer_urls: Option<Vec<String>>,
pub transfer_parameters: Option<LoginTransferParameters>,
}
/// For some reason, the `oauth` field in the login response is a string of JSON, not a JSON object.
/// Deserializes to `Option` because the `oauth` field is not always there.
fn oauth_data_from_string<'de, D>(deserializer: D) -> Result<Option<OAuthData>, D::Error>
where
D: Deserializer<'de>,
{
// for some reason, deserializing to &str doesn't work but this does.
let s: String = Deserialize::deserialize(deserializer)?;
let data: OAuthData = serde_json::from_str(s.as_str()).map_err(serde::de::Error::custom)?;
Ok(Some(data))
}
impl LoginResponse {
pub fn needs_transfer_login(&self) -> bool {
self.transfer_urls.is_some() || self.transfer_parameters.is_some()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_oauth_data_parse() {
// This example is from a login response that did not contain any transfer URLs.
let oauth: OAuthData = serde_json::from_str("{\"steamid\":\"78562647129469312\",\"account_name\":\"feuarus\",\"oauth_token\":\"fd2fdb3d0717bcd2220d98c7ec61c7bd\",\"wgtoken\":\"72E7013D598A4F68C7E268F6FA3767D89D763732\",\"wgtoken_secure\":\"21061EA13C36D7C29812CAED900A215171AD13A2\",\"webcookie\":\"6298070A226E5DAD49938D78BCF36F7A7118FDD5\"}").unwrap();
assert_eq!(oauth.steamid, "78562647129469312");
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bcd2220d98c7ec61c7bd");
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
assert_eq!(
oauth.wgtoken_secure,
"21061EA13C36D7C29812CAED900A215171AD13A2"
);
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
}
#[test]
fn test_login_response_parse() {
let result = serde_json::from_str::<LoginResponse>(include_str!(
"../fixtures/api-responses/login-response1.json"
));
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap();
let oauth = resp.oauth.unwrap();
assert_eq!(oauth.steamid, "78562647129469312");
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bad2220d98c7ec61c7bd");
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
assert_eq!(
oauth.wgtoken_secure,
"21061EA13C36D7C29812CAED900A215171AD13A2"
);
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
}
#[test]
fn test_login_response_parse_missing_webcookie() {
let result = serde_json::from_str::<LoginResponse>(include_str!(
"../fixtures/api-responses/login-response-missing-webcookie.json"
));
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap();
let oauth = resp.oauth.unwrap();
assert_eq!(oauth.steamid, "92591609556178617");
assert_eq!(oauth.oauth_token, "1cc83205dab2979e558534dab29f6f3aa");
assert_eq!(oauth.wgtoken, "3EDA9DEF07D7B39361D95203525D8AFE82A");
assert_eq!(oauth.wgtoken_secure, "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48");
assert_eq!(oauth.webcookie, "");
}
}

View file

@ -0,0 +1,23 @@
mod ITwoFactorService;
mod login;
mod phone_ajax;
pub use login::*;
pub use phone_ajax::*;
pub use ITwoFactorService::*;
use serde::{Deserialize, Deserializer};
pub(crate) fn parse_json_string_as_number<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
// for some reason, deserializing to &str doesn't work but this does.
let s: String = Deserialize::deserialize(deserializer)?;
Ok(s.parse().unwrap())
}
#[derive(Debug, Clone, Deserialize)]
pub struct SteamApiResponse<T> {
pub response: T,
}

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct PhoneValidateResponse {
success: bool,
number: String,
is_valid: bool,
is_voip: bool,
is_fixed: bool,
}

View file

@ -4,7 +4,6 @@ use anyhow::Result;
pub use confirmation::{Confirmation, ConfirmationType};
use hmacsha1::hmac_sha1;
use log::*;
use regex::Regex;
use reqwest::{
cookie::CookieStore,
header::{COOKIE, USER_AGENT},
@ -24,6 +23,7 @@ extern crate anyhow;
extern crate maplit;
mod accountlinker;
mod api_responses;
mod confirmation;
mod secret_string;
pub mod steamapi;

View file

@ -1,5 +1,4 @@
use crate::token::TwoFactorSecret;
use crate::SteamGuardAccount;
use crate::api_responses::*;
use log::*;
use reqwest::{
blocking::RequestBuilder,
@ -9,7 +8,7 @@ use reqwest::{
Url,
};
use secrecy::{CloneableSecret, DebugSecret, ExposeSecret, SerializableSecret};
use serde::{Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::iter::FromIterator;
use std::str::FromStr;
@ -21,77 +20,6 @@ lazy_static! {
static ref STEAM_API_BASE: String = "https://api.steampowered.com".into();
}
#[derive(Debug, Clone, Deserialize)]
pub struct LoginResponse {
pub success: bool,
#[serde(default)]
pub login_complete: bool,
#[serde(default)]
pub captcha_needed: bool,
#[serde(default)]
pub captcha_gid: String,
#[serde(default, deserialize_with = "parse_json_string_as_number")]
pub emailsteamid: u64,
#[serde(default)]
pub emailauth_needed: bool,
#[serde(default)]
pub requires_twofactor: bool,
#[serde(default)]
pub message: String,
// #[serde(rename = "oauth")]
// oauth_raw: String,
#[serde(default, deserialize_with = "oauth_data_from_string")]
oauth: Option<OAuthData>,
transfer_urls: Option<Vec<String>>,
transfer_parameters: Option<LoginTransferParameters>,
}
/// For some reason, the `oauth` field in the login response is a string of JSON, not a JSON object.
/// Deserializes to `Option` because the `oauth` field is not always there.
fn oauth_data_from_string<'de, D>(deserializer: D) -> Result<Option<OAuthData>, D::Error>
where
D: Deserializer<'de>,
{
// for some reason, deserializing to &str doesn't work but this does.
let s: String = Deserialize::deserialize(deserializer)?;
let data: OAuthData = serde_json::from_str(s.as_str()).map_err(serde::de::Error::custom)?;
Ok(Some(data))
}
impl LoginResponse {
pub fn needs_transfer_login(&self) -> bool {
self.transfer_urls.is_some() || self.transfer_parameters.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LoginTransferParameters {
steamid: String,
token_secure: String,
auth: String,
remember_login: bool,
webcookie: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RsaResponse {
pub success: bool,
pub publickey_exp: String,
pub publickey_mod: String,
pub timestamp: String,
pub token_gid: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OAuthData {
oauth_token: String,
steamid: String,
wgtoken: String,
wgtoken_secure: String,
#[serde(default)]
webcookie: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)]
#[zeroize(drop)]
pub struct Session {
@ -113,24 +41,6 @@ impl SerializableSecret for Session {}
impl CloneableSecret for Session {}
impl DebugSecret for Session {}
/// Represents the response from `/ITwoFactorService/QueryTime/v0001`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryTimeResponse {
/// The time that the server will use to check your two factor code.
#[serde(deserialize_with = "parse_json_string_as_number")]
pub server_time: u64,
#[serde(deserialize_with = "parse_json_string_as_number")]
pub skew_tolerance_seconds: u64,
#[serde(deserialize_with = "parse_json_string_as_number")]
pub large_time_jink: u64,
pub probe_frequency_seconds: u64,
pub adjusted_time_probe_frequency_seconds: u64,
pub hint_probe_frequency_seconds: u64,
pub sync_timeout: u64,
pub try_again_seconds: u64,
pub max_attempts: u64,
}
/// Queries Steam for the current time.
///
/// Endpoint: `/ITwoFactorService/QueryTime/v0001`
@ -565,189 +475,3 @@ impl SteamApiClient {
return Ok(resp.response);
}
}
#[test]
fn test_oauth_data_parse() {
// This example is from a login response that did not contain any transfer URLs.
let oauth: OAuthData = serde_json::from_str("{\"steamid\":\"78562647129469312\",\"account_name\":\"feuarus\",\"oauth_token\":\"fd2fdb3d0717bcd2220d98c7ec61c7bd\",\"wgtoken\":\"72E7013D598A4F68C7E268F6FA3767D89D763732\",\"wgtoken_secure\":\"21061EA13C36D7C29812CAED900A215171AD13A2\",\"webcookie\":\"6298070A226E5DAD49938D78BCF36F7A7118FDD5\"}").unwrap();
assert_eq!(oauth.steamid, "78562647129469312");
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bcd2220d98c7ec61c7bd");
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
assert_eq!(
oauth.wgtoken_secure,
"21061EA13C36D7C29812CAED900A215171AD13A2"
);
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
}
#[test]
fn test_login_response_parse() {
let result = serde_json::from_str::<LoginResponse>(include_str!(
"fixtures/api-responses/login-response1.json"
));
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap();
let oauth = resp.oauth.unwrap();
assert_eq!(oauth.steamid, "78562647129469312");
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bad2220d98c7ec61c7bd");
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
assert_eq!(
oauth.wgtoken_secure,
"21061EA13C36D7C29812CAED900A215171AD13A2"
);
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
}
#[test]
fn test_login_response_parse_missing_webcookie() {
let result = serde_json::from_str::<LoginResponse>(include_str!(
"fixtures/api-responses/login-response-missing-webcookie.json"
));
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap();
let oauth = resp.oauth.unwrap();
assert_eq!(oauth.steamid, "92591609556178617");
assert_eq!(oauth.oauth_token, "1cc83205dab2979e558534dab29f6f3aa");
assert_eq!(oauth.wgtoken, "3EDA9DEF07D7B39361D95203525D8AFE82A");
assert_eq!(oauth.wgtoken_secure, "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48");
assert_eq!(oauth.webcookie, "");
}
#[derive(Debug, Clone, Deserialize)]
pub struct SteamApiResponse<T> {
pub response: T,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AddAuthenticatorResponse {
/// Shared secret between server and authenticator
#[serde(default)]
pub shared_secret: String,
/// Authenticator serial number (unique per token)
#[serde(default)]
pub serial_number: String,
/// code used to revoke authenticator
#[serde(default)]
pub revocation_code: String,
/// URI for QR code generation
#[serde(default)]
pub uri: String,
/// Current server time
#[serde(default, deserialize_with = "parse_json_string_as_number")]
pub server_time: u64,
/// Account name to display on token client
#[serde(default)]
pub account_name: String,
/// Token GID assigned by server
#[serde(default)]
pub token_gid: String,
/// Secret used for identity attestation (e.g., for eventing)
#[serde(default)]
pub identity_secret: String,
/// Spare shared secret
#[serde(default)]
pub secret_1: String,
/// Result code
pub status: i32,
#[serde(default)]
pub phone_number_hint: Option<String>,
}
impl AddAuthenticatorResponse {
pub fn to_steam_guard_account(self) -> SteamGuardAccount {
SteamGuardAccount {
shared_secret: TwoFactorSecret::parse_shared_secret(self.shared_secret).unwrap(),
serial_number: self.serial_number.clone(),
revocation_code: self.revocation_code.into(),
uri: self.uri.into(),
server_time: self.server_time,
account_name: self.account_name.clone(),
token_gid: self.token_gid.clone(),
identity_secret: self.identity_secret.into(),
secret_1: self.secret_1.into(),
fully_enrolled: false,
device_id: "".into(),
session: None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct FinalizeAddAuthenticatorResponse {
pub status: i32,
#[serde(deserialize_with = "parse_json_string_as_number")]
pub server_time: u64,
pub want_more: bool,
pub success: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct PhoneValidateResponse {
success: bool,
number: String,
is_valid: bool,
is_voip: bool,
is_fixed: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RemoveAuthenticatorResponse {
pub success: bool,
}
fn parse_json_string_as_number<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
// for some reason, deserializing to &str doesn't work but this does.
let s: String = Deserialize::deserialize(deserializer)?;
Ok(s.parse().unwrap())
}
#[test]
fn test_parse_add_auth_response() {
let result = serde_json::from_str::<SteamApiResponse<AddAuthenticatorResponse>>(include_str!(
"fixtures/api-responses/add-authenticator-1.json"
));
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap().response;
assert_eq!(resp.server_time, 1628559846);
assert_eq!(resp.shared_secret, "wGwZx=sX5MmTxi6QgA3Gi");
assert_eq!(resp.revocation_code, "R123456");
}
#[test]
fn test_parse_add_auth_response2() {
let result = serde_json::from_str::<SteamApiResponse<AddAuthenticatorResponse>>(include_str!(
"fixtures/api-responses/add-authenticator-2.json"
));
assert!(
matches!(result, Ok(_)),
"got error: {}",
result.unwrap_err()
);
let resp = result.unwrap().response;
assert_eq!(resp.status, 29);
}

View file

@ -1,4 +1,7 @@
use crate::steamapi::{LoginResponse, RsaResponse, Session, SteamApiClient};
use crate::{
api_responses::{LoginResponse, RsaResponse},
steamapi::{Session, SteamApiClient},
};
use log::*;
use rsa::{PublicKey, RsaPublicKey};
use secrecy::ExposeSecret;