1use std::borrow::Cow;
2use std::fmt;
3
4use crate::core::PrivateKey;
5use crate::env::Environment;
6use crate::env::auth::url;
7use crate::signature;
8use base64::Engine;
9use base64::prelude::BASE64_STANDARD;
10use bluefin_api::apis::auth_api::{auth_token_refresh_put, auth_v2_token_post};
11use bluefin_api::apis::configuration::Configuration;
12use bluefin_api::models::{LoginRequest, LoginResponse, RefreshTokenRequest, RefreshTokenResponse};
13use secp256k1::Message;
14use sui_crypto::SuiSigner;
15use sui_crypto::ed25519::Ed25519PrivateKey;
16use sui_sdk_types::{PersonalMessage, SignatureScheme};
17
18#[derive(Debug)]
19pub enum Error {
20 AuthenticationRequestFailed(String),
21 AuthenticationRequestSerializationFailed(String),
22}
23
24impl fmt::Display for Error {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Error::AuthenticationRequestFailed(error) => {
28 write!(f, "Authentication request failed: {error}")
29 }
30 Error::AuthenticationRequestSerializationFailed(error) => {
31 write!(f, "Authentication request serialization failed: {error}")
32 }
33 }
34 }
35}
36
37impl std::error::Error for Error {}
38
39type AuthenticationResult<T> = Result<T, Error>;
40
41#[derive(Default)]
43pub struct AuthenticationOptions {
44 pub refresh_token_valid_for_seconds: Option<i64>,
46 pub read_only: Option<bool>,
50}
51
52pub trait Authenticate {
53 fn authenticate(
60 self,
61 signature: &str,
62 environment: Environment,
63 ) -> impl std::future::Future<Output = AuthenticationResult<LoginResponse>> + Send;
64
65 fn authenticate_with_options(
75 self,
76 signature: &str,
77 environment: Environment,
78 options: AuthenticationOptions,
79 ) -> impl std::future::Future<Output = AuthenticationResult<LoginResponse>> + Send;
80}
81
82pub trait RequestExt: Sized {
83 fn signature(
89 &self,
90 scheme: SignatureScheme,
91 private_key: PrivateKey,
92 ) -> signature::Result<String>;
93}
94
95pub trait Refresh {
96 fn refresh(
97 self,
98 environment: Environment,
99 ) -> impl std::future::Future<Output = AuthenticationResult<RefreshTokenResponse>> + Send;
100}
101
102impl RequestExt for LoginRequest {
103 fn signature(
109 &self,
110 scheme: SignatureScheme,
111 private_key: PrivateKey,
112 ) -> signature::Result<String> {
113 let bytes = serde_json::to_vec(self).map_err(|_| signature::Error::Serialization)?;
114
115 let personal_message = PersonalMessage(Cow::Borrowed(bytes.as_slice()));
116
117 match scheme {
118 SignatureScheme::Ed25519 => {
119 let private_key = Ed25519PrivateKey::new(private_key);
120
121 let signature = private_key
122 .sign_personal_message(&personal_message)
123 .map_err(|e| signature::Error::Signature(e.to_string()))?;
124
125 Ok(signature.to_base64())
126 }
127 SignatureScheme::Secp256k1 => {
128 const RECOVERY_CODE: u8 = 31;
129
130 let secp = secp256k1::Secp256k1::signing_only();
131 let private_key = secp256k1::SecretKey::from_byte_array(&private_key)
132 .map_err(|error| signature::Error::PrivateKey(error.to_string()))?;
133
134 let signature = secp.sign_ecdsa_recoverable(
135 &Message::from_digest(personal_message.signing_digest()),
136 &private_key,
137 );
138
139 let public_key = private_key.public_key(&secp256k1::Secp256k1::signing_only());
140
141 let (recovery_id, signature) = signature.serialize_compact();
142
143 let mut components = vec![SignatureScheme::Secp256k1 as u8];
144 components.push(
145 RECOVERY_CODE
146 + u8::try_from(i32::from(recovery_id))
147 .map_err(|_| signature::Error::PublicKeyRecoveryId)?,
148 );
149 components.extend(signature);
150 components.extend(public_key.serialize());
151
152 Ok(BASE64_STANDARD.encode(&components))
153 }
154 _ => Err(signature::Error::UnsupportedSignatureScheme(scheme)),
155 }
156 }
157}
158
159impl Authenticate for LoginRequest {
160 async fn authenticate(
161 self,
162 signature: &str,
163 environment: Environment,
164 ) -> AuthenticationResult<LoginResponse> {
165 self.authenticate_with_options(signature, environment, AuthenticationOptions::default())
166 .await
167 }
168
169 async fn authenticate_with_options(
170 self,
171 signature: &str,
172 environment: Environment,
173 options: AuthenticationOptions,
174 ) -> AuthenticationResult<LoginResponse> {
175 let base_url = url(environment);
176
177 let mut configuration = Configuration::new();
178 configuration.base_path = String::from(base_url);
179
180 let response = auth_v2_token_post(
181 &configuration,
182 signature,
183 self,
184 options.refresh_token_valid_for_seconds,
185 options.read_only,
186 )
187 .await
188 .map_err(|error| Error::AuthenticationRequestFailed(error.to_string()))?;
189
190 Ok(response)
191 }
192}
193
194impl Refresh for RefreshTokenRequest {
195 async fn refresh(self, environment: Environment) -> AuthenticationResult<RefreshTokenResponse> {
196 let base_url = url(environment);
197
198 let mut configuration = Configuration::new();
199 configuration.base_path = String::from(base_url);
200
201 let response = auth_token_refresh_put(&configuration, self)
202 .await
203 .map_err(|error| Error::AuthenticationRequestFailed(error.to_string()))?;
204
205 Ok(response)
206 }
207}
208
209#[cfg(test)]
210pub mod tests {
211 use crate::env;
212
213 use super::*;
214 use base64::Engine;
215 use base64::prelude::BASE64_STANDARD;
216 use chrono::Utc;
217 use rand::rngs::OsRng;
218 use secp256k1::Message;
219 use secp256k1::ecdsa::RecoveryId;
220 use sui_crypto::{SuiVerifier, ed25519::Ed25519Verifier};
221 use sui_sdk_types::{Ed25519PublicKey, Secp256k1PublicKey, SimpleSignature, UserSignature};
222
223 fn verify_ed25519_signature(
224 encoded_signature: &str,
225 login_payload: &LoginRequest,
226 ) -> Result<(), Box<dyn std::error::Error>> {
227 let signature = UserSignature::from_base64(encoded_signature)
228 .map_err(|_| "Could not base64 decode signature".to_string())?;
229
230 let bytes = serde_json::to_vec(login_payload)
231 .map_err(|_| "Could not serialize auth request".to_string())?;
232
233 let personal_message = PersonalMessage(Cow::Borrowed(bytes.as_slice()));
234
235 match signature {
236 UserSignature::Simple(SimpleSignature::Ed25519 { public_key, .. }) => {
237 let sui_address = public_key.derive_address().to_hex();
238
239 Ed25519Verifier::new()
240 .verify_personal_message(&personal_message, &signature)
241 .map_err(|_| "Invalid signature".to_string())?;
242
243 assert_eq!(sui_address, login_payload.account_address);
244 }
245 _ => Err("Unsupported signature scheme".to_string())?,
246 }
247
248 Ok(())
249 }
250
251 fn verify_secp256k1_signature(
253 encoded_signature: &str,
254 login_payload: &LoginRequest,
255 ) -> Result<(), Box<dyn std::error::Error>> {
256 const RECOVERY_CODE: u8 = 31;
257 let signature_bytes = BASE64_STANDARD
258 .decode(encoded_signature)
259 .map_err(|_| "Could not base64 decode signature".to_string())?;
260 if signature_bytes.len() != 99 {
264 return Err("Invalid signature length".into());
266 }
267 let recovery_bit = signature_bytes[1] - RECOVERY_CODE;
268
269 let signature = secp256k1::ecdsa::RecoverableSignature::from_compact(
270 &signature_bytes[2..signature_bytes.len() - 33],
271 RecoveryId::try_from(i32::from(recovery_bit))
272 .map_err(|_| "Invalid secp256k1 recovery ID".to_string())?,
273 )?;
274 let public_key = secp256k1::PublicKey::from_slice(&signature_bytes[(1 + 65)..])?;
275
276 let bytes = serde_json::to_vec(login_payload)
278 .map_err(|_| "Could not serialize auth request".to_string())?;
279 let personal_message = PersonalMessage(Cow::Borrowed(bytes.as_slice()));
280
281 let message = Message::from_digest(personal_message.signing_digest());
283
284 let recovered_public_key = signature
285 .recover(&message)
286 .map_err(|_| "Invalid secp256k1 signature".to_string())?;
287
288 assert_eq!(public_key, recovered_public_key);
289 Ok(())
290 }
291
292 #[test]
293 fn sign_auth_request() -> Result<(), Box<dyn std::error::Error>> {
294 let private_key = ed25519_dalek::SigningKey::generate(&mut OsRng);
296 let public_key = private_key.verifying_key().to_bytes();
297 let public_key = Ed25519PublicKey::new(public_key);
298
299 let sui_address = public_key.derive_address().to_hex();
300
301 let auth_request = LoginRequest {
302 account_address: sui_address,
303 audience: env::auth::staging::AUDIENCE.into(),
304 signed_at_millis: Utc::now().timestamp_millis(),
305 };
306
307 let signature = auth_request
308 .signature(SignatureScheme::Ed25519, private_key.to_bytes())
309 .map_err(|error| format!("{error:?}"))?;
310 verify_ed25519_signature(&signature, &auth_request)?;
311
312 let signature = auth_request
313 .signature(SignatureScheme::Secp256k1, private_key.to_bytes())
314 .map_err(|error| format!("{error:?}"))?;
315 verify_secp256k1_signature(&signature, &auth_request)?;
316
317 Ok(())
318 }
319
320 #[tokio::test]
321 async fn authenticate_staging_ed25519() -> Result<(), Box<dyn std::error::Error>> {
322 let private_key = ed25519_dalek::SigningKey::generate(&mut OsRng);
323 let public_key = private_key.verifying_key().to_bytes();
324 let public_key = Ed25519PublicKey::new(public_key);
325
326 let sui_address = public_key.derive_address().to_hex();
327
328 let auth_request = LoginRequest {
329 account_address: sui_address,
330 audience: env::auth::staging::AUDIENCE.into(),
331 signed_at_millis: Utc::now().timestamp_millis(),
332 };
333
334 let signature = auth_request
335 .signature(SignatureScheme::Ed25519, private_key.to_bytes())
336 .map_err(|error| format!("{error:?}"))?;
337
338 auth_request
339 .authenticate(&signature, Environment::Staging)
340 .await
341 .map_err(|error| format!("{error:?}"))?;
342 Ok(())
343 }
344
345 #[tokio::test]
346 async fn authenticate_staging_ed25519_with_options() -> Result<(), Box<dyn std::error::Error>> {
347 let private_key = ed25519_dalek::SigningKey::generate(&mut OsRng);
348 let public_key = private_key.verifying_key().to_bytes();
349 let public_key = Ed25519PublicKey::new(public_key);
350
351 let sui_address = public_key.derive_address().to_hex();
352
353 let auth_request = LoginRequest {
354 account_address: sui_address,
355 audience: env::auth::staging::AUDIENCE.into(),
356 signed_at_millis: Utc::now().timestamp_millis(),
357 };
358
359 let signature = auth_request
360 .signature(SignatureScheme::Ed25519, private_key.to_bytes())
361 .map_err(|error| format!("{error:?}"))?;
362
363 let response = auth_request
364 .authenticate_with_options(
365 &signature,
366 Environment::Staging,
367 AuthenticationOptions {
368 read_only: Some(true),
369 refresh_token_valid_for_seconds: Some(10),
370 },
371 )
372 .await
373 .map_err(|error| format!("{error:?}"))?;
374
375 assert_eq!(response.refresh_token_valid_for_seconds, 10);
376 Ok(())
377 }
378
379 #[tokio::test]
380 async fn authenticate_staging_secp256k1() -> Result<(), Box<dyn std::error::Error>> {
381 let (private_key, public_key) = secp256k1::generate_keypair(&mut OsRng);
382 let public_key = Secp256k1PublicKey::new(public_key.serialize());
383
384 let sui_address = public_key.derive_address().to_hex();
385
386 let auth_request = LoginRequest {
387 account_address: sui_address,
388 audience: env::auth::staging::AUDIENCE.into(),
389 signed_at_millis: Utc::now().timestamp_millis(),
390 };
391
392 let signature = auth_request
393 .signature(SignatureScheme::Secp256k1, private_key.secret_bytes())
394 .map_err(|error| format!("{error:?}"))?;
395
396 auth_request
397 .authenticate(&signature, Environment::Staging)
398 .await
399 .map_err(|error| format!("{error:?}"))?;
400 Ok(())
401 }
402}