1use std::time::SystemTime;
2
3use base64::{Engine, prelude::BASE64_STANDARD};
4use p256::{
5 ecdsa::{Signature, SigningKey, signature::Signer},
6 elliptic_curve::rand_core::OsRng,
7};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use crate::{DateTime, Error, api::hba_service, client::Client};
12
13pub const URL: &str = "https://auth.roblox.com/v1";
14
15#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
16pub enum LoginType {
17 Email,
18 #[default]
19 Username,
20 PhoneNumber,
21 EmailOtpSessionToken,
22 AuthToken,
23 Passkey,
24}
25
26#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
27pub enum MediaType {
28 Email,
29 SMS,
30 Authenticator,
31 RecoveryCode,
32 SecurityKey,
33 CrossDevice,
34 Password,
35 Passkey,
36}
37
38#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
39pub struct RecommendedUsernamesFromDisplayName {
40 #[serde(rename = "didGenerateNewUsername")]
41 pub new_name_generated: bool,
42 #[serde(rename = "suggestedUsernames")]
43 pub suggested_names: Vec<String>,
44}
45
46#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
47pub struct User {
48 pub id: u64,
49 pub name: String,
50 #[serde(rename = "displayName")]
51 pub display_name: String,
52}
53
54#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
55pub struct TwoStepVerificationInfo {
56 #[serde(rename = "mediaType")]
57 pub media_type: MediaType,
58 pub ticket: String,
59}
60
61#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
62pub struct LoginResponse {
63 pub user: User,
64 #[serde(rename = "twoStepVerificationData")]
65 pub two_step_verification_info: TwoStepVerificationInfo,
66 #[serde(rename = "identityVerificationLoginTicket")]
67 pub verification_ticket: String,
68 #[serde(rename = "isBanned")]
69 pub is_banned: bool,
70 #[serde(rename = "shouldUpdateEmail")]
71 pub should_update_email: bool,
72 #[serde(rename = "recoveryEmail")]
73 pub recovery_email: String,
74 #[serde(rename = "accountBlob")]
75 pub account_blob: String,
76}
77
78#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
79struct AuthenticationIntent {
80 #[serde(rename = "clientPublicKey")]
81 public_key: String,
82 #[serde(rename = "clientEpochTimestamp")]
83 epoch_timestamp: u64,
84 #[serde(rename = "saiSignature")]
85 signature: String,
86 #[serde(rename = "serverNonce")]
87 nonce: String,
88}
89
90async fn authentication_intent(client: &mut Client) -> Result<AuthenticationIntent, Error> {
91 let nonce = hba_service::v1::server_nonce(client).await?;
92
93 let unix = SystemTime::now()
94 .duration_since(SystemTime::UNIX_EPOCH)
95 .unwrap()
96 .as_secs();
97
98 let key = SigningKey::random(&mut OsRng);
99 let public_key = BASE64_STANDARD.encode(key.verifying_key().to_sec1_bytes());
100
101 let binding = format!("{}:{}:{}", public_key, unix, nonce);
102 let hash = Sha256::digest(binding);
103
104 let signature: Signature = key.sign(&hash[..]);
105 let signature = String::from_utf8_lossy(&signature.to_bytes()).to_string();
106
107 Ok(AuthenticationIntent {
108 public_key,
109 epoch_timestamp: unix,
110 signature,
111 nonce,
112 })
113}
114
115pub async fn login(
116 client: &mut Client,
117 login: &str,
118 key: &str,
119 login_type: LoginType,
120) -> Result<LoginResponse, Error> {
121 #[derive(Serialize)]
122 struct Request<'a> {
123 #[serde(rename = "ctype")]
124 login_type: LoginType,
125 #[serde(rename = "cvalue")]
126 login: &'a str,
127 #[serde(rename = "password")]
128 key: &'a str,
129
130 #[serde(rename = "secureAuthenticationIntent")]
131 authentication_intent: AuthenticationIntent,
132 }
133
134 let authentication_intent = authentication_intent(client).await?;
135 let result = client
136 .requestor
137 .client
138 .post(format!("{URL}/login"))
139 .headers(client.requestor.default_headers.clone())
140 .json(&Request {
141 login_type,
142 login,
143 key,
144 authentication_intent,
145 })
146 .send()
147 .await;
148
149 let response = client.validate_response(result).await?;
150 client.requestor.parse_json::<LoginResponse>(response).await
151}
152
153pub async fn recommended_usernames_from_display_name(
154 client: &mut Client,
155 display_name: &str,
156 birthday: DateTime,
157) -> Result<RecommendedUsernamesFromDisplayName, Error> {
158 #[derive(Serialize)]
159 struct Request<'a> {
160 #[serde(rename = "displayName")]
161 display_name: &'a str,
162 birthday: &'a str,
163 }
164
165 let result = client
166 .requestor
167 .client
168 .post(format!(
169 "{URL}/validators/recommendedUsernameFromDisplayName"
170 ))
171 .headers(client.requestor.default_headers.clone())
172 .json(&Request {
173 display_name,
174 birthday: birthday.to_string().as_str(),
175 })
176 .send()
177 .await;
178
179 let response = client.validate_response(result).await?;
180 client
181 .requestor
182 .parse_json::<RecommendedUsernamesFromDisplayName>(response)
183 .await
184}