diff --git a/Cargo.lock b/Cargo.lock index 09de94c..de74a00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,12 @@ dependencies = [ "num-traits 0.2.19", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "async-stream" version = "0.3.6" @@ -207,6 +213,30 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" +[[package]] +name = "binrw" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -300,6 +330,46 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "colorchoice" version = "1.0.3" @@ -593,10 +663,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + [[package]] name = "gimli" version = "0.31.1" @@ -644,6 +726,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1184,7 +1272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1217,6 +1305,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits 0.2.19", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1227,6 +1329,15 @@ dependencies = [ "num-traits 0.2.19", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits 0.2.19", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1242,6 +1353,28 @@ dependencies = [ "num-traits 0.2.19", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits 0.2.19", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -1318,6 +1451,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1392,6 +1531,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polariton" +version = "0.1.0" +dependencies = [ + "binrw", +] + +[[package]] +name = "polariton_auth" +version = "0.1.0" +dependencies = [ + "log", + "num", + "polariton", + "rand 0.9.0", + "ring", + "simple-rijndael", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1404,7 +1562,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1464,7 +1622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", - "getrandom", + "getrandom 0.2.15", "rand 0.8.5", "ring", "rustc-hash", @@ -1520,10 +1678,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.14", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -1534,6 +1703,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -1555,7 +1734,29 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.14", +] + +[[package]] +name = "rc_services" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "polariton", + "polariton_auth", + "tokio", ] [[package]] @@ -1701,7 +1902,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -1964,6 +2165,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-rijndael" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fc9945b08a629a18fb7eaf7d76d9e2ef2fa628c5ea1b7782627fc21bb2285" + [[package]] name = "simple_asn1" version = "0.6.3" @@ -2049,6 +2256,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef6b00f8fe8eaaaff22cb9b70822a48c1a5d772bc682c202a57c0b438175845" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2105,7 +2318,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2508,7 +2721,7 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "rand 0.8.5", ] @@ -2539,6 +2752,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2866,6 +3088,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -2918,7 +3149,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", ] [[package]] @@ -2932,6 +3172,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 4133d12..cf90889 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [workspace] members = [ - "auth", "rc_static_data", + "auth", "polariton_auth", "rc_services", "rc_static_data", ] [workspace.dependencies] @@ -13,3 +13,5 @@ rocket = { version = "0.5.1", features = [ "json" ] } libfj = { version = "0.7.5", path = "../libfj" } log = "0.4" env_logger = "0.11" +clap = { version = "4.5", features = [ "derive" ] } +polariton = { version = "*", path = "../polariton" } diff --git a/assets/robocraft/service_packets.md b/assets/robocraft/service_packets.md new file mode 100644 index 0000000..233f247 --- /dev/null +++ b/assets/robocraft/service_packets.md @@ -0,0 +1,65 @@ +# Connect to Photon + +## TCP packet + +Header 7 bytes +Connect Data 41 bytes + +### Header + +0..1 =251 (mode?) +1..5 packet length (big endian) +5..6 =0 channel ID +6..7 =1 reliable? (bool; 1->reliable, 0->unreliable) + +### Connect Data + +0..1 =243 1 byte (mode?) +1..2 =0 +2..4 Version 2 bytes (always 1 6) +4..5 =30 1 byte (client ID shifted by 1 -- 15 is ID) +5..7 client version 3 bytes (4 bytes merged into 3 -- 0 is upper 4 bits of byte 0, 1 is OR-ed with 0, byte 1 is 2, byte 2 is 3) +8..9 =0 +9..41 app ID string 32 bytes (if shorter than 32 bytes/chars the remaining bytes are 0; if longer than 32 bytes/chars the remaining app ID is ignored i.e. truncated) + +### Ping Response Data + +0..1 =240 1 byte +1..5 tick count at time of server's receipt (big endian) (unit of game ticks?) +5..9 tick count from client's request (big endian) (unit of game ticks?) + +### Ping Request Data +0..1 =240 +1..5 current tick count + +### Modes + +220 Region info (in) +222 Friends list info (in) +225 Join lobby info (in) +226 Join lobby info (in) +227 Join lobby info (in) +228 Master server connect success (in) +229 Lobby joined success (in) +230 Join/connect info (in) +231 Join/connect info (in) +240 Ping (both) +243 Connect (out) +244 Connect? response? (in) +254 Disconnect to Reconnect + +### Parameters + +#### Op code 230 + +192 Reset/redo Encryption? (Map) +202 Nickname (string) +225 User ID (string) +230 Master server address (string) + +#### Op code always??? +221 Auth token (string) + + + + diff --git a/polariton_auth/Cargo.toml b/polariton_auth/Cargo.toml new file mode 100644 index 0000000..e1a2974 --- /dev/null +++ b/polariton_auth/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "polariton_auth" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +polariton.workspace = true +num = "0.4" +rand = "0.9" +ring = "0.17" +simple-rijndael = "0.3" diff --git a/polariton_auth/src/encryption.rs b/polariton_auth/src/encryption.rs new file mode 100644 index 0000000..96a62fa --- /dev/null +++ b/polariton_auth/src/encryption.rs @@ -0,0 +1,86 @@ +use num::BigInt; +use rand::Rng; +use simple_rijndael::impls::RijndaelCbc; +use simple_rijndael::paddings::Pkcs7Padding; + +pub struct Keys { + pub pub_key: Vec, + pub enc: CryptoImpl, +} + +const SECRET_LEN: usize = 160 / 8; +const PRIME_768: &[u8] = &[255, 255, 255, 255, 255, 255, 255, 255, 201, 15, + 218, 162, 33, 104, 194, 52, 196, 198, 98, 139, + 128, 220, 28, 209, 41, 2, 78, 8, 138, 103, + 204, 116, 2, 11, 190, 166, 59, 19, 155, 34, + 81, 74, 8, 121, 142, 52, 4, 221, 239, 149, + 25, 179, 205, 58, 67, 27, 48, 43, 10, 109, + 242, 95, 20, 55, 79, 225, 53, 109, 109, 81, + 194, 69, 228, 133, 181, 118, 98, 94, 126, 198, + 244, 76, 66, 233, 166, 58, 54, 32, 255, 255, + 255, 255, 255, 255, 255, 255]; +const PRIME_ROOT: u8 = 22; + +pub fn generate_encryption_details(client_pub_key: &[u8]) -> Keys { + let client_num = BigInt::from_bytes_be(num::bigint::Sign::Plus, client_pub_key); + let big_0 = BigInt::from(0); + let prime_root = BigInt::from(PRIME_ROOT); + let my_prime = BigInt::from_bytes_be(num::bigint::Sign::Plus, PRIME_768); + log::debug!("Generating keys for client pub key {}", client_num.to_string()); + let mut rng = rand::rng(); + let mut bytes = rng.random::<[u8; SECRET_LEN]>(); + let mut my_secret = BigInt::from_bytes_be(num::bigint::Sign::Plus, &bytes); + while my_secret >= &my_prime - 1 || my_secret == big_0 { + bytes = rng.random::<[u8; SECRET_LEN]>(); + my_secret = BigInt::from_bytes_be(num::bigint::Sign::Plus, &bytes); + log::debug!("Generated secret {} (prime to beat: {})", my_secret.to_string(), my_prime.to_string()); + } + let my_pub_key = prime_root.modpow(&my_secret, &my_prime); + let shared_key = client_num.modpow(&my_secret, &my_prime); + log::debug!("Generated shared key {} and pub key {}", shared_key.to_string(), my_pub_key.to_string()); + let shared_key = shared_key.to_bytes_be().1; + let enc_key: Vec = ring::digest::digest(&ring::digest::SHA256, &shared_key).as_ref().into(); + log::debug!("Encryption key is {:?}", enc_key.as_slice()); + Keys { + pub_key: my_pub_key.to_signed_bytes_be(), + enc: CryptoImpl::new(enc_key), + } +} + +pub struct CryptoImpl { + crypto: RijndaelCbc, + iv: [u8; 16], + key: Vec +} + +impl CryptoImpl { + fn new(key: Vec) -> Self { + Self { + crypto: RijndaelCbc::::new(&key, 16).unwrap(), + iv: [0u8; 16], + key, + } + } + + fn decrypt(&self, data: Vec) -> Vec { + self.crypto.decrypt(&self.iv, data).unwrap_or_default() + } + + fn encrypt(&self, data: Vec) -> Vec { + self.crypto.encrypt(&self.iv, data).unwrap_or_default() + } +} + +impl polariton::packet::Cryptographer for CryptoImpl { + fn decrypt(&self, data: Vec) -> Vec { + self.decrypt(data) + } + + fn encrypt(&self, data: Vec) -> Vec { + self.encrypt(data) + } + + fn secret(&self) -> &'_ [u8] { + self.key.as_slice() + } +} diff --git a/polariton_auth/src/handshake.rs b/polariton_auth/src/handshake.rs new file mode 100644 index 0000000..db1b899 --- /dev/null +++ b/polariton_auth/src/handshake.rs @@ -0,0 +1,198 @@ +use std::sync::Arc; + +use polariton::packet::{Packet, Message, StandardMessage, Data, Cryptographer}; +use polariton::operation::{Typed, ParameterTable, OperationResponse}; + +#[derive(Debug)] +pub struct Handshake { + state: T, +} + +pub struct HandshakeAnd { + pub handshake: Handshake, + pub extra: X, +} + +pub struct Start<'a> { + app_id: &'a str, +} + +#[derive(Debug)] +pub enum ConnectError<'b, 'a> { + UnexpectedPacket, + WrongAppId { got: &'b str, expected: &'a str }, +} + +// TODO impl core::fmt::Display for ConnectError +// TODO impl std::error::Error for ConnectError + +impl <'a> Handshake> { + pub fn new(app_id: &'a str) -> Self { + Self { + state: Start { app_id }, + } + } + + pub fn connect<'b>(self, packet: &'b Packet) -> Result, HandshakeAnd, ConnectError<'b, 'a>>> { + if let Packet::Packet(packet) = &packet { + if let Message::Standard(conn) = &packet.message { + if let Data::InitStart(info) = &conn.data { + if info.app_id != self.state.app_id { + let err = ConnectError::WrongAppId { got: &info.app_id, expected: &self.state.app_id }; + return Err(HandshakeAnd { + handshake: self, + extra: err, + }); + } + let new_self = Handshake:: { + state: Connected, + }; + return Ok(HandshakeAnd { + handshake: new_self, + extra: Packet::from_message( + Message::Standard(StandardMessage { + flags: 0, + data: Data::InitAck , + }), + packet.header.channel, true, None).unwrap() + }); + } + } + } + Err(HandshakeAnd { + handshake: self, + extra: ConnectError::UnexpectedPacket, + }) + } +} + +pub struct Connected; + +#[derive(Debug)] +pub enum EncryptError { + UnexpectedPacket, + MissingParameter(u8), +} + +impl Handshake { + const PUBLIC_KEY_PARAM_KEY: u8 = 1; + + pub fn encrypt<'a>(self, packet: &'a Packet) -> Result>)>, HandshakeAnd> { + if let Packet::Packet(packet) = &packet { + if let Message::Standard(conn) = &packet.message { + if let Data::InternalOpReq(req) = &conn.data { + if req.code == 0 { + let params = req.params.to_owned().to_dict(); + if let Some(Typed::Bytes(pub_key)) = params.get(&Self::PUBLIC_KEY_PARAM_KEY) { + let keys = crate::encryption::generate_encryption_details(&pub_key.vec); + let mut response_params = std::collections::HashMap::with_capacity(1); + response_params.insert(Self::PUBLIC_KEY_PARAM_KEY, Typed::Bytes(keys.pub_key.into())); + let resp_packet = Packet::from_message( + Message::Standard( + StandardMessage { + flags: 0, + data: Data::InternalOpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: ParameterTable::from_dict(response_params) }) + }), 0, true, None).unwrap(); + let new_self = Handshake:: { + state: Encrypted, + }; + return Ok(HandshakeAnd { + handshake: new_self, + extra: (resp_packet, Box::new(Arc::new(keys.enc))), + }); + } else { + return Err(HandshakeAnd { + handshake: self, + extra: EncryptError::MissingParameter(Self::PUBLIC_KEY_PARAM_KEY), + }); + } + } + + } + } + } + Err(HandshakeAnd { + handshake: self, + extra: EncryptError::UnexpectedPacket, + }) + } +} + +pub struct Encrypted; + +impl Handshake { + pub fn with_auth, E>(self, auth: T) -> Handshake> { + Handshake { + state: Auth { + authenticator: auth, + _e: Default::default(), + } + } + } +} + +pub trait AuthProvider { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, E>; +} + +pub struct Auth, E> { + authenticator: T, + _e: std::marker::PhantomData, +} + +#[derive(Debug)] +pub enum AuthError { + Validation(E), + UnexpectedPacket +} + +impl , E> Handshake> { + //const SERVER_ADDRESS_KEY: u8 = 230; + const AUTH_REQUEST_CODE: u8 = 230; + const USER_ID_KEY: u8 = 225; + const NICKNAME_KEY: u8 = 225; + pub fn authenticate<'a>(mut self, packet: &'a Packet, crypto: Box>) -> Result, AuthError>> { + if let Packet::Packet(packet) = &packet { + if let Message::Standard(conn) = &packet.message { + if let Data::OpReq(req) = &conn.data { + if req.code == Self::AUTH_REQUEST_CODE /* or 231 ???*/ { + let req_dict = req.params.to_owned().to_dict(); + let mut params_resp = match self.state.authenticator.validate(&req_dict) { + Ok(x) => x, + Err(e) => { + return Err(HandshakeAnd { + handshake: self, + extra: AuthError::Validation(e), + }); + } + }; + if let Some(user_id) = req_dict.get(&Self::USER_ID_KEY) { + params_resp.insert(Self::USER_ID_KEY, user_id.to_owned()); + params_resp.insert(Self::NICKNAME_KEY, user_id.to_owned()); + } + return Ok(Packet::from_message( + Message::Standard( + StandardMessage { + flags: 0, + data: Data::OpResp(OperationResponse { + code: Self::AUTH_REQUEST_CODE, + return_code: 0, + message: Typed::Null, + params: params_resp.into(), + }) + }.encrypt(conn.is_encrypted()) + ), packet.header.channel, true, Some(crypto)).unwrap()); + } + } + } + } + Err(HandshakeAnd { + handshake: self, + extra: AuthError::UnexpectedPacket, + }) + } +} diff --git a/polariton_auth/src/lib.rs b/polariton_auth/src/lib.rs new file mode 100644 index 0000000..c8344d7 --- /dev/null +++ b/polariton_auth/src/lib.rs @@ -0,0 +1,7 @@ +mod encryption; + +mod handshake; +pub use handshake::{Handshake, AuthProvider, AuthError}; + +mod ping_pong; +pub use ping_pong::ping_pong; diff --git a/polariton_auth/src/ping_pong.rs b/polariton_auth/src/ping_pong.rs new file mode 100644 index 0000000..812e799 --- /dev/null +++ b/polariton_auth/src/ping_pong.rs @@ -0,0 +1,11 @@ +use polariton::packet::Ping; + +#[inline] +pub fn ping_pong(mut ping: Ping) -> Ping { + if ping.tick2.is_some() { + ping.tick2 = None; + } else { + ping.tick2 = Some(ping.tick1); + } + ping +} diff --git a/rc_services/Cargo.toml b/rc_services/Cargo.toml new file mode 100644 index 0000000..5d67f9c --- /dev/null +++ b/rc_services/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rc_services" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +clap.workspace = true +polariton.workspace = true +polariton_auth = { version = "*", path = "../polariton_auth" } diff --git a/rc_services/build_arm64.sh b/rc_services/build_arm64.sh new file mode 100755 index 0000000..7010ff6 --- /dev/null +++ b/rc_services/build_arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release --target aarch64-unknown-linux-musl diff --git a/rc_services/run_debug.sh b/rc_services/run_debug.sh new file mode 100755 index 0000000..eedcd2c --- /dev/null +++ b/rc_services/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_BACKTRACE=1 RUST_LOG=debug cargo run diff --git a/rc_services/src/cli.rs b/rc_services/src/cli.rs new file mode 100644 index 0000000..6b72f08 --- /dev/null +++ b/rc_services/src/cli.rs @@ -0,0 +1,31 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 4532)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Socket read tries before giving up (0 to never give up) + #[arg(long, default_value_t = 5)] + pub retries: usize, + + /// Domain and port of the game server to send new connections + #[arg(long, default_value_t = {"127.0.0.1:4533".to_string()})] + pub redirect: String, + + /// Name of game server to send new connections + #[arg(long, default_value_t = {"ngram_is_ngnius".to_string()})] + pub room_name: String, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_services/src/main.rs b/rc_services/src/main.rs new file mode 100644 index 0000000..dba4ede --- /dev/null +++ b/rc_services/src/main.rs @@ -0,0 +1,340 @@ +mod cli; +mod state; + +use std::num::NonZero; +use std::sync::Arc; + +use polariton_auth::Handshake; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net; + +use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket}; +use polariton::operation::{OperationResponse, Typed}; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let args = cli::CliArgs::get(); + log::debug!("Got cli args {:?}", args); + let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); + + let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; + + loop { + let (socket, address) = listener.accept().await?; + process_socket(socket, address, NonZero::new(args.retries), &args.redirect, &args.room_name).await; + } +} + +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, game_server_url: &str, game_server_name: &str) { + log::debug!("Accepting connection from address {}", address); + + let mut buf = Vec::new(); + let enc = match do_connect_handshake(&mut buf, &mut socket, retries, game_server_url, game_server_name).await { + Some(x) => x, + None => { + log::error!("Failed to do connect handshake with {}", address); + return; + } + }; + buf.clear(); +} + +async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) { + let resp = Packet::Ping(polariton_auth::ping_pong(ping)); + resp.to_buf(buf, None).unwrap(); + let write_count = socket.write(buf).await.unwrap(); + log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf); +} + +fn buf_likely_valid(buf: &[u8]) -> bool { + buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC +} + +async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result { + let read_count = socket.read_buf(buf).await?; + log::debug!("Read {} bytes from socket: {:?}", read_count, buf); + Ok(read_count) +} + +async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result { + buf.clear(); + let read_count = read_more(buf, socket).await?; + if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet + let mut last_err = None; + let mut must_succeed_next = false; + if let Some(max_retries) = max_retries { + for _ in 0..max_retries.get() { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("(connect) Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } else { + while buf_likely_valid(buf.as_slice()) { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("(connect) Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } +} + +async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> { + log::debug!("Sending packet {:?}", packet); + buf.clear(); + packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + let write_count = socket.write(buf).await?; + log::debug!("Write {} bytes to socket: {:?}", write_count, buf); + #[cfg(debug_assertions)] + { + // print out unencrypted packet too + if let Packet::Packet(standard_p) = packet { + if let Message::Standard(standard_m) = standard_p.message { + if standard_m.is_encrypted() { + let standard_m = standard_m.encrypt(false); + let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) }); + packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len()); + } + + } + } + } + Ok(()) +} + +const APP_ID: &str = "WebServicesServer"; + +struct AuthImpl; + +const TOKEN_KEY: u8 = 216; // token;refresh_token +//const UNKNOWN_BYTE_KEY: u8 = 217; +const SERVICE_KEY: u8 = 224; +const USERNAME_KEY: u8 = 225; + +//const CCU_KEY: u8 = 245; + +#[derive(Debug)] +enum AuthError { + WrongService { expected: String, actual: String }, + MissingService, + MissingToken, + MissingUsername, +} + +impl AuthError { + fn log_err(&self) { + match self { + Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected), + Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY), + Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY), + Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY), + } + } +} + +impl polariton_auth::AuthProvider for AuthImpl { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> { + if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) { + if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) { + if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) { + if service.string == APP_ID { + let params_resp = std::collections::HashMap::::new(); + //params_resp.insert(CCU_KEY, Typed::Byte(0)); + log::debug!("Auth success for {} (token: {})", user.string, token.string); + Ok(params_resp) + } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) } + } else { Err(AuthError::MissingUsername) } + } else { Err(AuthError::MissingService) } + } else { Err(AuthError::MissingToken) } + } +} + +async fn do_connect_handshake( + buf: &mut Vec, + socket: &mut net::TcpStream, + max_retries: Option>, + game_server_url: &str, + game_server_name: &str, +) -> Option>> { + let handshake = Handshake::new(APP_ID); + // connect + log::debug!("(connect) Handling first packet"); + let packet1 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read connect packet: {}", e); + return None; + } + }; + let (handshake, to_send) = match handshake.connect(&packet1) { + Ok(x) => (x.handshake, x.extra), + Err(e) => { + log::error!("Failed to handle connect handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send connect ack packet: {}", e); + return None; + } + } + // encrypt + log::debug!("(connect) Handling second packet"); + let mut packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet2 { + handle_ping(ping, buf, socket).await; + packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + } + let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) { + Ok(x) => (x.handshake, x.extra.0, x.extra.1), + Err(e) => { + log::error!("Failed to handle encryption handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send encryption ack packet: {}", e); + return None; + } + } + // pre-auth + let handshake = handshake.with_auth(AuthImpl); + // authenticate + log::debug!("(connect) Handling third packet"); + let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet3 { + handle_ping(ping, buf, socket).await; + packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + } + let to_send = match handshake.authenticate(&packet3, crypto.clone()) { + Ok(x) => x, + Err(h) => match h.extra { + polariton_auth::AuthError::Validation(e) => { + e.log_err(); + return None; + }, + e => { + log::error!("Failed to handle auth handshake: {:?}", e); + return None; + }, + }, + }; + match send_packet(to_send, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send auth ack packet: {}", e); + return None; + } + } + + // send CCU passed event + let ccu_passed_event = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::Event(polariton::operation::Event { + code: 14, // CCU passed event code for Web service + params: std::collections::HashMap::new().into(), + }), + }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap(); + match send_packet(ccu_passed_event, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send CCU event packet: {}", e); + return None; + } + } + let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet_j { + handle_ping(ping, buf, socket).await; + packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + } + if let Packet::Packet(msg) = &packet_j { + if let Message::Standard(st) = &msg.message { + if let Data::OpReq(req) = &st.data { + if req.code == 225 { // join lobby + log::debug!("Max players from lobby join request: {:?}", req.params.to_owned().to_dict().get(&255)); + let mut params = std::collections::HashMap::::new(); + params.insert(230 /* game server address */, Typed::Str(game_server_url.into())); + params.insert(255 /* room name */, Typed::Str(game_server_name.into())); + let resp = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::OpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: params.into(), + }), + }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap(); + match send_packet(resp, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send lobby ack packet: {}", e); + return None; + } + } + } + } + } + } + + Some(crypto) +} diff --git a/rc_services/src/state.rs b/rc_services/src/state.rs new file mode 100644 index 0000000..2bd18ab --- /dev/null +++ b/rc_services/src/state.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +pub struct State { + pub crypto: Box>, +} + +impl State { + pub fn new(c: Box>) -> Self { + Self { + crypto: c, + } + } + + pub fn binrw_args(&self) -> polariton::packet::WriteArgs { + Some(self.crypto.clone()) + } +}