From 23b7c278bb82d25ac44542beaef14df7ee6d7e56 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 11 Jul 2024 18:15:13 +0200 Subject: [PATCH] Add initial game connection token implementation --- Cargo.toml | 7 +- src/config.rs | 28 +++++- src/game_connection.rs | 34 +++++++ src/game_connection_token.rs | 174 +++++++++++++++++++++++++++++++++++ src/main.rs | 6 +- src/players.rs | 20 ++-- tsom_api_config.toml.default | 11 ++- 7 files changed, 264 insertions(+), 16 deletions(-) create mode 100644 src/game_connection.rs create mode 100644 src/game_connection_token.rs diff --git a/Cargo.toml b/Cargo.toml index 7f1acfd..9753c46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,11 @@ edition = "2021" actix-governor = "0.5" actix-web = "4.4" base64 = "0.22" -cached = { version = "0.51", features = ["async"] } +cached = { version = "0.52", features = ["async"] } +chacha20poly1305 = "0.10" confy = "0.6" deadpool-postgres = "0.14" +deku = "0.17" env_logger = "0.11" futures = "0.3" octocrab = "0.38" @@ -20,8 +22,9 @@ reqwest = { version = "0.12", features = ["charset", "http2", "macos-system-conf secure-string = { version = "0.3", features = ["serde"] } semver = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_with = { version = "3.8", features = ["base64", "time_0_3"] } strum = { version = "0.26", features = ["derive"] } tokio = "1.37" tokio-postgres = { version = "0.7", features = ["with-uuid-1"] } url = "2.5" -uuid = { version = "1.8", features = ["v4", "macro-diagnostics"] } \ No newline at end of file +uuid = { version = "1.8", features = ["v4", "macro-diagnostics"] } diff --git a/src/config.rs b/src/config.rs index 3b83cbf..b55a25e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,12 @@ +use std::time::Duration; + use secure_string::SecureString; use serde::{Deserialize, Serialize}; +use serde_with::base64::Base64; +use serde_with::serde_as; +use serde_with::DurationSeconds; +#[serde_as] #[derive(Serialize, Deserialize)] pub struct ApiConfig { pub listen_address: String, @@ -9,7 +15,8 @@ pub struct ApiConfig { pub game_repository: String, pub updater_repository: String, pub updater_filename: String, - pub cache_lifespan: u64, + #[serde_as(as = "DurationSeconds")] + pub cache_lifespan: Duration, pub github_pat: Option, pub db_host: String, pub db_user: String, @@ -17,6 +24,14 @@ pub struct ApiConfig { pub db_database: String, pub player_nickname_maxlength: usize, pub player_allow_non_ascii: bool, + pub game_api_token: String, + pub game_api_url: String, + pub game_server_address: String, + pub game_server_port: u16, + #[serde_as(as = "DurationSeconds")] + pub game_api_token_duration: Duration, + #[serde_as(as = "Base64")] + pub connection_token_key: [u8; 32], } impl Default for ApiConfig { @@ -28,7 +43,7 @@ impl Default for ApiConfig { game_repository: "ThisSpaceOfMine".to_string(), updater_filename: "this_updater_of_mine".to_string(), updater_repository: "ThisUpdaterOfMine".to_string(), - cache_lifespan: 5 * 60, + cache_lifespan: Duration::from_secs(5 * 60), github_pat: None, db_host: "localhost".to_string(), db_user: "api".to_string(), @@ -36,6 +51,15 @@ impl Default for ApiConfig { db_database: "tsom_db".to_string(), player_nickname_maxlength: 16, player_allow_non_ascii: false, + game_api_token: "".into(), + game_api_url: "http://localhost".to_string(), + game_server_address: "localhost".to_string(), + game_server_port: 29536, + game_api_token_duration: Duration::from_secs(5 * 60), + connection_token_key: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, + ], } } } diff --git a/src/game_connection.rs b/src/game_connection.rs new file mode 100644 index 0000000..bf5d076 --- /dev/null +++ b/src/game_connection.rs @@ -0,0 +1,34 @@ +use actix_web::{post, web, HttpResponse, Responder}; +use uuid::Uuid; + +use crate::{ + app_data::AppData, + errors::api::RouteError, + game_connection_token::{ + GameConnectionToken, GameConnectionTokenPrivate, GamePlayerData, GameServerAddress, + }, +}; + +#[post("/v1/player/test")] +async fn player_test(app_data: web::Data) -> Result { + let player_data = GamePlayerData::generate(Uuid::new_v4(), "SirLynix".into()); + + let server_address = GameServerAddress { + address: app_data.config.game_server_address.clone(), + port: app_data.config.game_server_port, + }; + + let private_token = GameConnectionTokenPrivate::generate( + app_data.config.game_api_url.clone(), + app_data.config.game_api_token.clone(), + player_data, + ); + let token = GameConnectionToken::generate( + app_data.config.connection_token_key.into(), + app_data.config.game_api_token_duration, + server_address, + private_token, + ); + + Ok(HttpResponse::Ok().json(token)) +} diff --git a/src/game_connection_token.rs b/src/game_connection_token.rs new file mode 100644 index 0000000..310c6ca --- /dev/null +++ b/src/game_connection_token.rs @@ -0,0 +1,174 @@ +use chacha20poly1305::{ + aead::{AeadCore, AeadMutInPlace, KeyInit, OsRng}, + XChaCha20Poly1305, +}; +use deku::prelude::*; +use serde::Serialize; +use serde_with::{base64::Base64, serde_as}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +const XCHACHA20POLY1305_IETF_ABYTES: usize = 16; //< todo: Get it from chacha20poly1305::Tag + +#[serde_as] +#[derive(Debug, Serialize)] +pub struct GameEncryptionKeys { + #[serde_as(as = "Base64")] + pub client_to_server: chacha20poly1305::Key, + #[serde_as(as = "Base64")] + pub server_to_client: chacha20poly1305::Key, +} + +impl GameEncryptionKeys { + pub fn generate() -> Self { + Self { + client_to_server: XChaCha20Poly1305::generate_key(&mut OsRng), + server_to_client: XChaCha20Poly1305::generate_key(&mut OsRng), + } + } +} + +#[derive(Debug, Serialize)] +pub struct GameServerAddress { + pub address: String, + pub port: u16, +} + +#[derive(Debug, DekuWrite)] +#[deku(endian = "little")] +pub struct GameConnectionTokenAdditionalData { + pub token_version: u32, + pub expire_timestamp: u64, + #[deku(writer = "deku_helper_write_key(deku::writer, &self.client_to_server_key)")] + pub client_to_server_key: chacha20poly1305::Key, + #[deku(writer = "deku_helper_write_key(deku::writer, &self.server_to_client_key)")] + pub server_to_client_key: chacha20poly1305::Key, +} + +#[serde_as] +#[derive(Debug, Serialize)] +pub struct GameConnectionToken { + token_version: u32, + #[serde_as(as = "Base64")] + token_nonce: chacha20poly1305::XNonce, + creation_timestamp: u64, + expire_timestamp: u64, + encryption_keys: GameEncryptionKeys, + game_server: GameServerAddress, + #[serde_as(as = "Base64")] + private_token_data: Vec, +} + +impl GameConnectionToken { + pub fn generate( + token_key: chacha20poly1305::Key, + duration: Duration, + server_address: GameServerAddress, + private_token: GameConnectionTokenPrivate, + ) -> Self { + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + + let encryption_keys = GameEncryptionKeys::generate(); + + let token_version = 1u32; + let expire_timestamp = (timestamp + duration).as_secs(); + + let additional_data = GameConnectionTokenAdditionalData { + token_version, + expire_timestamp, + client_to_server_key: encryption_keys.client_to_server.clone(), + server_to_client_key: encryption_keys.server_to_client.clone(), + }; + + let additional_data_bytes = additional_data.to_bytes().unwrap(); + + let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); + + let mut private_token_bytes = private_token.to_bytes().unwrap(); + private_token_bytes.resize(private_token_bytes.len() + XCHACHA20POLY1305_IETF_ABYTES, 0); + + let mut cipher = XChaCha20Poly1305::new(&token_key); + cipher + .encrypt_in_place( + &nonce, + additional_data_bytes.as_slice(), + &mut private_token_bytes, + ) + .unwrap(); + + Self { + token_version, + token_nonce: nonce, + creation_timestamp: timestamp.as_secs(), + expire_timestamp: (timestamp + duration).as_secs(), + encryption_keys, + game_server: server_address, + private_token_data: private_token_bytes, + } + } +} + +#[derive(Debug, DekuWrite)] +#[deku(endian = "endian", ctx = "endian: deku::ctx::Endian")] +pub struct GamePlayerData { + #[deku(writer = "deku_helper_write_uuid(deku::writer, &self.uuid)")] + uuid: Uuid, + #[deku(writer = "deku_helper_write_str(deku::writer, &self.nickname)")] + nickname: String, +} + +impl GamePlayerData { + pub fn generate(uuid: Uuid, nickname: String) -> Self { + Self { uuid, nickname } + } +} + +#[derive(Debug, DekuWrite)] +#[deku(endian = "little")] +pub struct GameConnectionTokenPrivate { + #[deku(writer = "deku_helper_write_str(deku::writer, &self.api_token)")] + api_token: String, + #[deku(writer = "deku_helper_write_str(deku::writer, &self.api_url)")] + api_url: String, + player_data: GamePlayerData, +} + +impl GameConnectionTokenPrivate { + pub fn generate( + game_api_url: String, + game_api_token: String, + player_data: GamePlayerData, + ) -> Self { + Self { + api_token: game_api_token, + api_url: game_api_url, + player_data, + } + } +} + +fn deku_helper_write_key( + writer: &mut Writer, + value: &chacha20poly1305::Key, +) -> Result<(), DekuError> { + let str_bytes = value.as_slice(); + str_bytes.to_writer(writer, ()) +} + +fn deku_helper_write_str( + writer: &mut Writer, + value: &str, +) -> Result<(), DekuError> { + let str_bytes = value.as_bytes(); + let str_len = str_bytes.len() as u32; + str_len.to_writer(writer, ())?; + str_bytes.to_writer(writer, ()) +} + +fn deku_helper_write_uuid( + writer: &mut Writer, + value: &Uuid, +) -> Result<(), DekuError> { + let str = value.to_bytes_le(); + str.to_writer(writer, ()) +} diff --git a/src/main.rs b/src/main.rs index 76bc8ca..f4c3c81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use actix_governor::{Governor, GovernorConfig, GovernorConfigBuilder}; use actix_web::{middleware, web, App, HttpServer}; use cached::TimedCache; use confy::ConfyError; +use game_connection::player_test; use std::sync::Mutex; use crate::app_data::AppData; @@ -14,6 +15,8 @@ mod app_data; mod config; mod errors; mod fetcher; +mod game_connection; +mod game_connection_token; mod game_data; mod players; mod version; @@ -65,7 +68,7 @@ async fn main() -> Result<(), std::io::Error> { let bind_address = format!("{}:{}", config.listen_address, config.listen_port); let data_config = web::Data::new(AppData { - cache: Mutex::new(TimedCache::with_lifespan(config.cache_lifespan)), // 5min + cache: Mutex::new(TimedCache::with_lifespan(config.cache_lifespan.as_secs())), // 5min config, fetcher, }); @@ -86,6 +89,7 @@ async fn main() -> Result<(), std::io::Error> { .app_data(pg_pool.clone()) .service(game_version) .service(player_authenticate) + .service(player_test) .service( web::scope("") .wrap(Governor::new(&player_create_governor_conf)) diff --git a/src/players.rs b/src/players.rs index 49b5f68..2409b47 100644 --- a/src/players.rs +++ b/src/players.rs @@ -17,7 +17,7 @@ struct CreatePlayerParams { #[derive(Serialize)] struct CreatePlayerResponse { - guid: String, + uuid: String, token: String, } @@ -57,13 +57,13 @@ async fn player_create( ))); } - let guid = Uuid::new_v4(); + let uuid = Uuid::new_v4(); let mut pg_client = pg_pool.get().await.unwrap(); let create_player_statement = pg_client .prepare_typed_cached( - "INSERT INTO players(guid, creation_time, nickname) VALUES($1, NOW(), $2) RETURNING id", + "INSERT INTO players(uuid, creation_time, nickname) VALUES($1, NOW(), $2) RETURNING id", &[Type::UUID, Type::VARCHAR], ) .await?; @@ -81,7 +81,7 @@ async fn player_create( let transaction = pg_client.transaction().await?; let result = transaction - .query(&create_player_statement, &[&guid, &nickname]) + .query(&create_player_statement, &[&uuid, &nickname]) .await?; let player_id: i32 = result[0].get(0); transaction @@ -90,11 +90,11 @@ async fn player_create( transaction.commit().await?; pg_client - .query(&create_player_statement, &[&guid, &nickname]) + .query(&create_player_statement, &[&uuid, &nickname]) .await?; Ok(HttpResponse::Ok().json(CreatePlayerResponse { - guid: guid.to_string(), + uuid: uuid.to_string(), token: token.to_string(), })) } @@ -106,7 +106,7 @@ struct AuthenticationParams { #[derive(Serialize)] struct AuthenticationResponse { - guid: String, + uuid: String, nickname: String, } @@ -135,7 +135,7 @@ async fn player_authenticate( let find_player_info = pg_client .prepare_typed_cached( - "SELECT guid, nickname FROM players WHERE id = $1", + "SELECT uuid, nickname FROM players WHERE id = $1", &[Type::INT4], ) .await?; @@ -151,11 +151,11 @@ async fn player_authenticate( let player_id: i32 = token_result[0].get(0); let player_result = pg_client.query(&find_player_info, &[&player_id]).await?; - let guid: Uuid = player_result[0].get(0); + let uuid: Uuid = player_result[0].get(0); let nickname: String = player_result[0].get(1); Ok(HttpResponse::Ok().json(AuthenticationResponse { - guid: guid.to_string(), + uuid: uuid.to_string(), nickname, })) } diff --git a/tsom_api_config.toml.default b/tsom_api_config.toml.default index 62a0662..9127519 100644 --- a/tsom_api_config.toml.default +++ b/tsom_api_config.toml.default @@ -4,7 +4,7 @@ repo_owner = "DigitalPulseSoftware" game_repository = "ThisSpaceOfMine" updater_repository = "ThisUpdaterOfMine" updater_filename = "this_updater_of_mine" -cache_lifespan = 300 # duration from second +cache_lifespan = 300 # duration in seconds # github_pat = "***" db_host = "localhost" db_user = "tsom" @@ -12,3 +12,12 @@ db_password = "" db_database = "tsom" player_nickname_maxlength = 16 player_allow_non_ascii = false + +connection_token_key = "123456" + +game_api_url = "http://localhost:14770" +game_api_token = "123456" +game_api_token_duration = 300 # duration in seconds + +game_server_address = "::1" +game_server_port = 29536