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, Deserialize, 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, Serialize, 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, Serialize, 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, Serialize, PartialEq, Eq)]
47#[serde(rename_all = "camelCase")]
48pub struct User {
49 pub id: u64,
50 pub name: String,
51 pub display_name: String,
52}
53
54#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
55#[serde(rename_all = "camelCase")]
56pub struct TwoStepVerificationInfo {
57 pub media_type: MediaType,
58 pub ticket: String,
59}
60
61#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
62#[serde(rename_all = "camelCase")]
63pub struct LoginResponse {
64 pub user: User,
65 #[serde(rename = "twoStepVerificationData")]
66 pub two_step_verification_info: TwoStepVerificationInfo,
67 #[serde(rename = "identityVerificationLoginTicket")]
68 pub verification_ticket: String,
69 pub is_banned: bool,
70 pub should_update_email: bool,
71 pub recovery_email: String,
72 pub account_blob: String,
73}
74
75#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
76struct AuthenticationIntent {
77 #[serde(rename = "clientPublicKey")]
78 public_key: String,
79 #[serde(rename = "clientEpochTimestamp")]
80 epoch_timestamp: u64,
81 #[serde(rename = "saiSignature")]
82 signature: String,
83 #[serde(rename = "serverNonce")]
84 nonce: String,
85}
86
87async fn authentication_intent(client: &mut Client) -> Result<AuthenticationIntent, Error> {
88 let nonce = hba_service::v1::server_nonce(client).await?;
89
90 let unix = SystemTime::now()
91 .duration_since(SystemTime::UNIX_EPOCH)
92 .unwrap()
93 .as_secs();
94
95 let key = SigningKey::random(&mut OsRng);
96 let public_key = BASE64_STANDARD.encode(key.verifying_key().to_sec1_bytes());
97
98 let binding = format!("{}:{}:{}", public_key, unix, nonce);
99 let hash = Sha256::digest(binding);
100
101 let signature: Signature = key.sign(&hash[..]);
102 let signature = String::from_utf8_lossy(&signature.to_bytes()).to_string();
103
104 Ok(AuthenticationIntent {
105 public_key,
106 epoch_timestamp: unix,
107 signature,
108 nonce,
109 })
110}
111
112pub async fn login(
113 client: &mut Client,
114 login: &str,
115 key: &str,
116 login_type: LoginType,
117) -> Result<LoginResponse, Error> {
118 #[derive(Serialize)]
119 struct Request<'a> {
120 #[serde(rename = "ctype")]
121 login_type: LoginType,
122 #[serde(rename = "cvalue")]
123 login: &'a str,
124 #[serde(rename = "password")]
125 key: &'a str,
126
127 #[serde(rename = "secureAuthenticationIntent")]
128 authentication_intent: AuthenticationIntent,
129 }
130
131 let authentication_intent = authentication_intent(client).await?;
132 let result = client
133 .requestor
134 .client
135 .post(format!("{URL}/login"))
136 .headers(client.requestor.default_headers.clone())
137 .json(&Request {
138 login_type,
139 login,
140 key,
141 authentication_intent,
142 })
143 .send()
144 .await;
145
146 let response = client.validate_response(result).await?;
147 client.requestor.parse_json::<LoginResponse>(response).await
148}
149
150pub async fn recommended_usernames_from_display_name(
151 client: &mut Client,
152 display_name: &str,
153 birthday: DateTime,
154) -> Result<RecommendedUsernamesFromDisplayName, Error> {
155 #[derive(Serialize)]
156 #[serde(rename_all = "camelCase")]
157 struct Request<'a> {
158 display_name: &'a str,
159 birthday: &'a str,
160 }
161
162 let result = client
163 .requestor
164 .client
165 .post(format!(
166 "{URL}/validators/recommendedUsernameFromDisplayName"
167 ))
168 .headers(client.requestor.default_headers.clone())
169 .json(&Request {
170 display_name,
171 birthday: birthday.to_string().as_str(),
172 })
173 .send()
174 .await;
175
176 let response = client.validate_response(result).await?;
177 client
178 .requestor
179 .parse_json::<RecommendedUsernamesFromDisplayName>(response)
180 .await
181}