From 68811c1a69d24d437355e544e92a7fde61b590d6 Mon Sep 17 00:00:00 2001 From: Ruben Adema <44266876+RadiatedMonkey@users.noreply.github.com> Date: Tue, 31 Dec 2024 04:14:34 +0100 Subject: [PATCH 01/11] decoding first login chain jwt --- crates/macros/src/lib.rs | 2 + crates/proto/Cargo.toml | 3 +- crates/proto/src/codec.rs | 6 + crates/proto/src/encryption.rs | 5 +- .../version/v729/types/connection_request.rs | 319 +++++++++++------- examples/proto/server.rs | 3 +- 6 files changed, 208 insertions(+), 130 deletions(-) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index c6dfdde7..c148bdc6 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -262,6 +262,8 @@ pub fn gamepackets(input: proc_macro::TokenStream) -> proc_macro::TokenStream { if let Some(v) = value { quote! { <#v as ::bedrockrs_proto_core::GamePacket>::ID => { + println!("Decoding {}", stringify!(#name)); + match <#v as ::bedrockrs_proto_core::ProtoCodec>::proto_deserialize(stream) { Ok(pk) => GamePackets::#name(pk), Err(e) => return Err(e), diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 8fee5b3d..b79ac964 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -25,12 +25,11 @@ base64 = "0.22" uuid = { version = "1.11", features = ["v4"] } serde_json = "1.0" tokio = { version = "1.40", features = ["full"] } +p384 = "0.13.0" flate2 = "1.0" snap = "1.1" -x509-cert = "0.2" - bitflags = "2.6.0" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/proto/src/codec.rs b/crates/proto/src/codec.rs index 8f749ad5..b3add841 100644 --- a/crates/proto/src/codec.rs +++ b/crates/proto/src/codec.rs @@ -27,9 +27,13 @@ pub fn decode_gamepackets( ) -> Result, ProtoCodecError> { log::trace!("Decoding gamepackets"); + println!("decrypt"); gamepacket_stream = decrypt_gamepackets::(gamepacket_stream, encryption)?; + println!("decompress"); gamepacket_stream = decompress_gamepackets::(gamepacket_stream, compression)?; + println!("separate"); let gamepackets = separate_gamepackets::(gamepacket_stream)?; + println!("done!"); Ok(gamepackets) } @@ -111,6 +115,8 @@ pub fn decrypt_gamepackets( mut gamepacket_stream: Vec, encryption: Option<&mut Encryption>, ) -> Result, ProtoCodecError> { + dbg!("Attempting to decrypt"); + if let Some(encryption) = encryption { gamepacket_stream = encryption.decrypt(gamepacket_stream)?; } diff --git a/crates/proto/src/encryption.rs b/crates/proto/src/encryption.rs index 9bf5e305..9a6729f7 100644 --- a/crates/proto/src/encryption.rs +++ b/crates/proto/src/encryption.rs @@ -2,6 +2,7 @@ use bedrockrs_proto_core::error::EncryptionError; #[derive(Debug, Clone)] pub struct Encryption { + recv_counter: u64, send_counter: u64, buf: [u8; 8], key: Vec, @@ -9,7 +10,9 @@ pub struct Encryption { impl Encryption { pub fn new() -> Self { - unimplemented!() + Self { + recv_counter: 0, send_counter: 0, buf: [0; 8], key: Vec::new() + } } pub fn decrypt(&mut self, _src: Vec) -> Result, EncryptionError> { diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index 8fd7c4ab..890d2503 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -6,9 +6,13 @@ use base64::Engine; use bedrockrs_proto_core::error::ProtoCodecError; use bedrockrs_proto_core::ProtoCodec; use byteorder::{LittleEndian, ReadBytesExt}; -use jsonwebtoken::{DecodingKey, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation}; +use serde::Deserialize; use serde_json::Value; use varint_rs::VarintReader; +use p384::pkcs8::spki; + +pub const MOJANG_PUBLIC_KEY: &str = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp"; #[derive(Debug, Clone)] pub struct ConnectionRequest { @@ -82,7 +86,7 @@ pub struct ConnectionRequest { fn read_i32_string(stream: &mut Cursor<&[u8]>) -> Result { let len = stream - .read_i32::()? + .read_u32::()? .try_into() .map_err(ProtoCodecError::FromIntError)?; @@ -92,6 +96,38 @@ fn read_i32_string(stream: &mut Cursor<&[u8]>) -> Result +} + +#[derive(Deserialize, Debug)] +struct KeyPayload { + #[serde(rename = "identityPublicKey")] + pub public_key: String +} + +fn parse_first_token(token: &str) -> Result { + let header = jsonwebtoken::decode_header(token)?; + let Some(base64_x5u) = header.x5u else { + todo!(); + }; + let bytes = BASE64_STANDARD.decode(base64_x5u)?; + + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { + unimplemented!("{e:?}"); + ProtoCodecError::LeftOvers(0) + })?; + + let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); + let mut validation = Validation::new(Algorithm::ES384); + validation.validate_exp = true; + validation.validate_nbf = true; + + let payload = jsonwebtoken::decode::(token, &decoding_key, &validation)?; + Ok(payload.claims.public_key) +} + impl ProtoCodec for ConnectionRequest { fn proto_serialize(&self, _stream: &mut Vec) -> Result<(), ProtoCodecError> where @@ -106,135 +142,166 @@ impl ProtoCodec for ConnectionRequest { where Self: Sized, { - let mut certificate_chain: Vec> = vec![]; - - // Read the ConnectionRequests length, Mojang stores it as a String - // (certificate_chain len + raw_token len + 8) - // 8 = i32 len + i32 len (length of certificate_chain's len and raw_token's len) - // can be ignored, other lengths are provided stream.read_u32_varint()?; - let certificate_chain_string = read_i32_string(stream)?; - - // parse certificate chain string into json - let mut certificate_chain_json = serde_json::from_str(&certificate_chain_string)?; - - let certificate_chain_json_jwts = match certificate_chain_json { - Value::Object(ref mut v) => { - let chain = v.get_mut("chain").ok_or(ProtoCodecError::FormatMismatch( - "Missing element chain in JWT certificate_chain", - ))?; - - match chain { - Value::Array(v) => v, - _ => { - // the certificate chain should always be an object with just an - // array of JWTs called "chain" - return Err(ProtoCodecError::FormatMismatch( - "Expected chain in JWT certificate_chain to be of type Array", - )); - } - } - } - _ => { - // the certificate chain should always be an object with just an array of - // JWTs called "chain" - return Err(ProtoCodecError::FormatMismatch( - "Expected Object in base of JWT certificate_chain", - )); - } - }; - - let mut key_data = vec![]; - - for jwt_json in certificate_chain_json_jwts { - let jwt_string = match jwt_json { - Value::String(str) => str, - _ => { - // the certificate chain's should always be a jwt string - return Err(ProtoCodecError::FormatMismatch( - "Expected chain array in certificate_chain to just contain Strings", - )); - } - }; - - // Extract header - let jwt_header = - jsonwebtoken::decode_header(jwt_string).map_err(ProtoCodecError::JwtError)?; - - let mut jwt_validation = Validation::new(jwt_header.alg); - // TODO: This definitely is not right. Even Zuri-MC doesn't understand this.. I may understand it.. I do understand it, update I don't. But I now know someone that does, I hope - // TODO: Someone else should find out how this works - jwt_validation.insecure_disable_signature_validation(); - jwt_validation.set_required_spec_claims::<&str>(&[]); - - // Is first jwt, use self-signed header from x5u - if key_data.is_empty() { - let x5u = jwt_header.x5u.ok_or(ProtoCodecError::FormatMismatch( - "Expected x5u in JWT header", - ))?; - - let x5u = x5u.as_bytes(); - - key_data = BASE64_STANDARD - .decode(x5u) - .map_err(ProtoCodecError::Base64DecodeError)?; - } + let cert_chain_json_len = stream.read_i32::()?; + let mut cert_chain_json = Vec::with_capacity(cert_chain_json_len as usize); + cert_chain_json.resize(cert_chain_json_len as usize, 0); + + stream.read_exact(&mut cert_chain_json)?; + + let cert_chain = serde_json::from_slice::(&cert_chain_json)?; + match cert_chain.chain.len() { + // User is offline + 1 => { + todo!() + }, + // Authenticated with Microsoft services + 3 => { + // Verify the first token and use its public key for the next token. - // Decode the jwt string into a jwt object - let jwt = jsonwebtoken::decode::>( - &jwt_string, - &DecodingKey::from_ec_der(&key_data), - &jwt_validation, - ) - .map_err(ProtoCodecError::JwtError)?; - - let identity_field = - jwt.claims - .get("identityPublicKey") - .ok_or(ProtoCodecError::FormatMismatch( - "Missing identityPublicKey field in JWT for validation", - ))?; - - key_data = match identity_field { - Value::String(str) => BASE64_STANDARD - .decode(str.as_bytes()) - .map_err(ProtoCodecError::Base64DecodeError)?, - _ => { - return Err(ProtoCodecError::FormatMismatch( - "Expected identityPublicKey field in JWT to be of type String", - )) - } - }; - - certificate_chain.push(jwt.claims); + let mut key = parse_first_token(&cert_chain.chain[0])?; + println!("key: {key}"); + }, + len => { + todo!() + } } - let raw_token_string = read_i32_string(stream)?; - - // Extract header - let raw_token_jwt_header = - jsonwebtoken::decode_header(&raw_token_string).map_err(ProtoCodecError::JwtError)?; - - let mut jwt_validation = Validation::new(raw_token_jwt_header.alg); - // TODO: This definitely is not right. Even Zuri-MC doesn't understand this.. I may understand it.. I do understand it, update I don't. - // TODO: Someone else should find out how this works - jwt_validation.insecure_disable_signature_validation(); - jwt_validation.set_required_spec_claims::<&str>(&[]); - - // Decode the jwt string into a jwt object - let raw_token = jsonwebtoken::decode::>( - &raw_token_string, - &DecodingKey::from_ec_der(&[]), - &jwt_validation, - ) - .map_err(ProtoCodecError::JwtError)? - .claims; - - Ok(Self { - certificate_chain, - raw_token, - }) + let header = jsonwebtoken::decode_header(&cert_chain.chain[0])?; + + + todo!() + + // let mut certificate_chain: Vec> = vec![]; + // + // // Read the ConnectionRequests length, Mojang stores it as a String + // // (certificate_chain len + raw_token len + 8) + // // 8 = i32 len + i32 len (length of certificate_chain's len and raw_token's len) + // // can be ignored, other lengths are provided + // stream.read_u32_varint()?; + // + // let certificate_chain_string = read_i32_string(stream)?; + // + // // parse certificate chain string into json + // let mut certificate_chain_json = serde_json::from_str(&certificate_chain_string)?; + // + // let certificate_chain_json_jwts = match certificate_chain_json { + // Value::Object(ref mut v) => { + // let chain = v.get_mut("chain").ok_or(ProtoCodecError::FormatMismatch( + // "Missing element chain in JWT certificate_chain", + // ))?; + // + // match chain { + // Value::Array(v) => v, + // _ => { + // // the certificate chain should always be an object with just an + // // array of JWTs called "chain" + // return Err(ProtoCodecError::FormatMismatch( + // "Expected chain in JWT certificate_chain to be of type Array", + // )); + // } + // } + // } + // _ => { + // // the certificate chain should always be an object with just an array of + // // JWTs called "chain" + // return Err(ProtoCodecError::FormatMismatch( + // "Expected Object in base of JWT certificate_chain", + // )); + // } + // }; + // + // let mut key_data = vec![]; + // + // for jwt_json in certificate_chain_json_jwts { + // let jwt_string = match jwt_json { + // Value::String(str) => str, + // _ => { + // // the certificate chain's should always be a jwt string + // return Err(ProtoCodecError::FormatMismatch( + // "Expected chain array in certificate_chain to just contain Strings", + // )); + // } + // }; + // + // // Extract header + // let jwt_header = + // jsonwebtoken::decode_header(jwt_string).map_err(ProtoCodecError::JwtError)?; + // + // let mut jwt_validation = Validation::new(jwt_header.alg); + // // TODO: This definitely is not right. Even Zuri-MC doesn't understand this.. I may understand it.. I do understand it, update I don't. But I now know someone that does, I hope + // // TODO: Someone else should find out how this works + // jwt_validation.insecure_disable_signature_validation(); + // jwt_validation.set_required_spec_claims::<&str>(&[]); + // + // // Is first jwt, use self-signed header from x5u + // if key_data.is_empty() { + // let x5u = jwt_header.x5u.ok_or(ProtoCodecError::FormatMismatch( + // "Expected x5u in JWT header", + // ))?; + // + // let x5u = x5u.as_bytes(); + // + // key_data = BASE64_STANDARD + // .decode(x5u) + // .map_err(ProtoCodecError::Base64DecodeError)?; + // } + // + // // Decode the jwt string into a jwt object + // let jwt = jsonwebtoken::decode::>( + // &jwt_string, + // &DecodingKey::from_ec_der(&key_data), + // &jwt_validation, + // ) + // .map_err(ProtoCodecError::JwtError)?; + // + // let identity_field = + // jwt.claims + // .get("identityPublicKey") + // .ok_or(ProtoCodecError::FormatMismatch( + // "Missing identityPublicKey field in JWT for validation", + // ))?; + // + // key_data = match identity_field { + // Value::String(str) => BASE64_STANDARD + // .decode(str.as_bytes()) + // .map_err(ProtoCodecError::Base64DecodeError)?, + // _ => { + // return Err(ProtoCodecError::FormatMismatch( + // "Expected identityPublicKey field in JWT to be of type String", + // )) + // } + // }; + // + // certificate_chain.push(jwt.claims); + // } + // + // let raw_token_string = read_i32_string(stream)?; + // + // // Extract header + // let raw_token_jwt_header = + // jsonwebtoken::decode_header(&raw_token_string).map_err(ProtoCodecError::JwtError)?; + // + // let mut jwt_validation = Validation::new(raw_token_jwt_header.alg); + // // TODO: This definitely is not right. Even Zuri-MC doesn't understand this.. I may understand it.. I do understand it, update I don't. + // // TODO: Someone else should find out how this works + // jwt_validation.insecure_disable_signature_validation(); + // jwt_validation.set_required_spec_claims::<&str>(&[]); + // + // // Decode the jwt string into a jwt object + // let raw_token = jsonwebtoken::decode::>( + // &raw_token_string, + // &DecodingKey::from_ec_der(&[]), + // &jwt_validation, + // ) + // .map_err(ProtoCodecError::JwtError)? + // .claims; + // + // Ok(Self { + // certificate_chain, + // raw_token, + // }) } fn get_size_prediction(&self) -> usize { diff --git a/examples/proto/server.rs b/examples/proto/server.rs index 2ce064a6..d5b15637 100644 --- a/examples/proto/server.rs +++ b/examples/proto/server.rs @@ -9,6 +9,7 @@ use bedrockrs_proto::v662::types::{BaseGameVersion, Experiments}; use bedrockrs_proto::v662::GamePackets; use bedrockrs_proto::v662::ProtoHelperV662; use tokio::time::Instant; +use bedrockrs_proto::v729::helper::ProtoHelperV729; #[tokio::main] async fn main() { @@ -59,7 +60,7 @@ async fn handle_login(mut conn: Connection) { conn.compression = Some(compression); // Login - conn.recv::().await.unwrap(); + conn.recv::().await.unwrap(); println!("Login"); conn.send::(&[ From f09d4285203ed29d96edafb977e5ae255cfe79eb Mon Sep 17 00:00:00 2001 From: Ruben Adema <44266876+RadiatedMonkey@users.noreply.github.com> Date: Tue, 31 Dec 2024 04:35:23 +0100 Subject: [PATCH 02/11] decode identity jwt in chain --- .../version/v729/types/connection_request.rs | 88 +++++++++++++++---- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index 890d2503..81032e13 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -11,6 +11,7 @@ use serde::Deserialize; use serde_json::Value; use varint_rs::VarintReader; use p384::pkcs8::spki; +use uuid::Uuid; pub const MOJANG_PUBLIC_KEY: &str = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp"; @@ -84,18 +85,6 @@ pub struct ConnectionRequest { pub raw_token: BTreeMap, } -fn read_i32_string(stream: &mut Cursor<&[u8]>) -> Result { - let len = stream - .read_u32::()? - .try_into() - .map_err(ProtoCodecError::FromIntError)?; - - let mut string_buf = vec![0; len]; - stream.read_exact(&mut string_buf)?; - - Ok(String::from_utf8(string_buf)?) -} - #[derive(Deserialize, Debug)] struct CertChain { pub chain: Vec @@ -107,7 +96,7 @@ struct KeyPayload { pub public_key: String } -fn parse_first_token(token: &str) -> Result { +fn parse_first_token(token: &str) -> Result { let header = jsonwebtoken::decode_header(token)?; let Some(base64_x5u) = header.x5u else { todo!(); @@ -124,10 +113,63 @@ fn parse_first_token(token: &str) -> Result { validation.validate_exp = true; validation.validate_nbf = true; + // Decode token + let payload = jsonwebtoken::decode::(token, &decoding_key, &validation)?; + Ok(payload.claims.public_key.eq(&MOJANG_PUBLIC_KEY)) +} + +fn parse_mojang_token(token: &str) -> Result { + let bytes = BASE64_STANDARD.decode(MOJANG_PUBLIC_KEY)?; + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { + unimplemented!("{e:?}"); + ProtoCodecError::LeftOvers(0) + })?; + + let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); + let mut validation = Validation::new(Algorithm::ES384); + validation.set_issuer(&["Mojang"]); + validation.validate_nbf = true; + validation.validate_exp = true; + let payload = jsonwebtoken::decode::(token, &decoding_key, &validation)?; Ok(payload.claims.public_key) } +#[derive(Deserialize, Debug)] +pub struct RawIdentity { + #[serde(rename = "XUID")] + pub xuid: String, + #[serde(rename = "displayName")] + pub display_name: String, + #[serde(rename = "identity")] + pub uuid: Uuid +} + +#[derive(Deserialize, Debug)] +struct IdentityPayload { + #[serde(rename = "extraData")] + pub client_data: RawIdentity, + #[serde(rename = "identityPublicKey")] + pub public_key: String +} + +fn parse_identity_token(token: &str, key: &str) -> Result { + let bytes = BASE64_STANDARD.decode(key)?; + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { + unimplemented!("{e:?}"); + ProtoCodecError::LeftOvers(0) + })?; + + let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); + let mut validation = Validation::new(Algorithm::ES384); + validation.set_issuer(&["Mojang"]); + validation.validate_exp = true; + validation.validate_nbf = true; + + let payload = jsonwebtoken::decode::(token, &decoding_key, &validation)?; + Ok(payload.claims) +} + impl ProtoCodec for ConnectionRequest { fn proto_serialize(&self, _stream: &mut Vec) -> Result<(), ProtoCodecError> where @@ -151,25 +193,33 @@ impl ProtoCodec for ConnectionRequest { stream.read_exact(&mut cert_chain_json)?; let cert_chain = serde_json::from_slice::(&cert_chain_json)?; + + let identity; match cert_chain.chain.len() { // User is offline 1 => { - todo!() + todo!(); }, // Authenticated with Microsoft services 3 => { // Verify the first token and use its public key for the next token. - let mut key = parse_first_token(&cert_chain.chain[0])?; - println!("key: {key}"); + let mut valid = parse_first_token(&cert_chain.chain[0])?; + if !valid { + // Login attempted using forged token chain. + todo!() + } + + let key = parse_mojang_token(&cert_chain.chain[1])?; + identity = parse_identity_token(&cert_chain.chain[2], &key)?; }, - len => { + // This should not happen... + _ => { todo!() } } - let header = jsonwebtoken::decode_header(&cert_chain.chain[0])?; - + dbg!(identity); todo!() From 60d2842b535285f1d88f200ea552fa3857147365 Mon Sep 17 00:00:00 2001 From: Ruben Adema <44266876+RadiatedMonkey@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:13:34 +0100 Subject: [PATCH 03/11] user data deserialization (excluding skin) --- crates/proto/Cargo.toml | 1 + .../src/version/v662/enums/build_platform.rs | 3 +- .../src/version/v662/enums/ui_profile.rs | 5 +- .../version/v729/types/connection_request.rs | 258 +++++++----------- crates/proto_core/src/error.rs | 3 +- crates/proto_core/src/types/string.rs | 2 +- 6 files changed, 105 insertions(+), 167 deletions(-) diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index b79ac964..756996cd 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -32,5 +32,6 @@ snap = "1.1" bitflags = "2.6.0" serde = { version = "1.0", features = ["derive"] } +serde_repr = "0.1.19" rak-rs = { version = "0.3", default-features = false, features = ["async_tokio", "mcpe"] } diff --git a/crates/proto/src/version/v662/enums/build_platform.rs b/crates/proto/src/version/v662/enums/build_platform.rs index 041421f7..eca77b6a 100644 --- a/crates/proto/src/version/v662/enums/build_platform.rs +++ b/crates/proto/src/version/v662/enums/build_platform.rs @@ -1,6 +1,7 @@ +use serde_repr::Deserialize_repr; use bedrockrs_macros::ProtoCodec; -#[derive(ProtoCodec, Clone, Debug)] +#[derive(ProtoCodec, Deserialize_repr, Clone, Debug)] #[enum_repr(i32)] #[enum_endianness(le)] #[repr(i32)] diff --git a/crates/proto/src/version/v662/enums/ui_profile.rs b/crates/proto/src/version/v662/enums/ui_profile.rs index 5010a271..2fe38df5 100644 --- a/crates/proto/src/version/v662/enums/ui_profile.rs +++ b/crates/proto/src/version/v662/enums/ui_profile.rs @@ -1,4 +1,7 @@ -/// UNUSED +use serde_repr::Deserialize_repr; + +#[derive(Deserialize_repr, Debug, Copy, Clone, PartialEq, Eq)] +#[repr(i32)] pub enum UIProfile { Classic = 0, Pocket = 1, diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index 81032e13..768641a4 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; use std::io::{Cursor, Read}; - +use std::string::FromUtf8Error; use base64::prelude::BASE64_STANDARD; use base64::Engine; use bedrockrs_proto_core::error::ProtoCodecError; @@ -12,6 +12,9 @@ use serde_json::Value; use varint_rs::VarintReader; use p384::pkcs8::spki; use uuid::Uuid; +use bedrockrs_addon::language::code::LanguageCode; +use crate::v662::enums::{BuildPlatform, UIProfile}; +use crate::v662::types::SerializedSkin; pub const MOJANG_PUBLIC_KEY: &str = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp"; @@ -170,6 +173,94 @@ fn parse_identity_token(token: &str, key: &str) -> Result) -> Result { + let cert_chain_json_len = stream.read_i32::()?; + let mut cert_chain_json = Vec::with_capacity(cert_chain_json_len as usize); + cert_chain_json.resize(cert_chain_json_len as usize, 0); + + stream.read_exact(&mut cert_chain_json)?; + + let cert_chain = serde_json::from_slice::(&cert_chain_json)?; + + let identity; + match cert_chain.chain.len() { + // User is offline + 1 => { + todo!(); + }, + // Authenticated with Microsoft services + 3 => { + // Verify the first token and use its public key for the next token. + + let mut valid = parse_first_token(&cert_chain.chain[0])?; + if !valid { + // Login attempted using forged token chain. + todo!() + } + + let key = parse_mojang_token(&cert_chain.chain[1])?; + identity = parse_identity_token(&cert_chain.chain[2], &key)?; + }, + // This should not happen... + _ => { + todo!() + } + } + + Ok(identity) +} + +#[derive(Deserialize, Debug)] +pub struct ClientInfo { + #[serde(rename = "DeviceOS")] + pub build_platform: BuildPlatform, + #[serde(rename = "DeviceModel")] + pub device_model: String, + #[serde(rename = "DeviceId")] + pub device_id: String, + #[serde(rename = "LanguageCode")] + pub language_code: String, // TODO: Use LanguageCode enum instead + #[serde(rename = "UIProfile")] + pub ui_profile: UIProfile, + #[serde(rename = "GuiScale")] + pub gui_scale: u32 +} + +#[derive(Deserialize, Debug)] +struct UserData { + #[serde(flatten)] + pub info: ClientInfo, + // #[serde(flatten)] + // pub skin: SerializedSkin +} + +fn parse_user_data_token(token: &str, key: &str) -> Result { + let bytes = BASE64_STANDARD.decode(key)?; + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { + unimplemented!("{e:?}"); + ProtoCodecError::LeftOvers(0) + })?; + + let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); + let mut validation = Validation::new(Algorithm::ES384); + + validation.required_spec_claims.clear(); + + let payload = jsonwebtoken::decode(token, &decoding_key, &validation)?; + Ok(payload.claims) +} + +fn parse_user_data(stream: &mut Cursor<&[u8]>, public_key: &str) -> Result { + let token_len = stream.read_i32::()?; + let mut token = Vec::with_capacity(token_len as usize); + token.resize(token_len as usize, 0); + + stream.read_exact(&mut token)?; + + let token_str = std::str::from_utf8(&token)?; + parse_user_data_token(token_str, public_key) +} + impl ProtoCodec for ConnectionRequest { fn proto_serialize(&self, _stream: &mut Vec) -> Result<(), ProtoCodecError> where @@ -186,172 +277,13 @@ impl ProtoCodec for ConnectionRequest { { stream.read_u32_varint()?; - let cert_chain_json_len = stream.read_i32::()?; - let mut cert_chain_json = Vec::with_capacity(cert_chain_json_len as usize); - cert_chain_json.resize(cert_chain_json_len as usize, 0); - - stream.read_exact(&mut cert_chain_json)?; - - let cert_chain = serde_json::from_slice::(&cert_chain_json)?; - - let identity; - match cert_chain.chain.len() { - // User is offline - 1 => { - todo!(); - }, - // Authenticated with Microsoft services - 3 => { - // Verify the first token and use its public key for the next token. - - let mut valid = parse_first_token(&cert_chain.chain[0])?; - if !valid { - // Login attempted using forged token chain. - todo!() - } - - let key = parse_mojang_token(&cert_chain.chain[1])?; - identity = parse_identity_token(&cert_chain.chain[2], &key)?; - }, - // This should not happen... - _ => { - todo!() - } - } + let identity = parse_identity(stream)?; + let user_data = parse_user_data(stream, &identity.public_key)?; dbg!(identity); + dbg!(user_data); todo!() - - // let mut certificate_chain: Vec> = vec![]; - // - // // Read the ConnectionRequests length, Mojang stores it as a String - // // (certificate_chain len + raw_token len + 8) - // // 8 = i32 len + i32 len (length of certificate_chain's len and raw_token's len) - // // can be ignored, other lengths are provided - // stream.read_u32_varint()?; - // - // let certificate_chain_string = read_i32_string(stream)?; - // - // // parse certificate chain string into json - // let mut certificate_chain_json = serde_json::from_str(&certificate_chain_string)?; - // - // let certificate_chain_json_jwts = match certificate_chain_json { - // Value::Object(ref mut v) => { - // let chain = v.get_mut("chain").ok_or(ProtoCodecError::FormatMismatch( - // "Missing element chain in JWT certificate_chain", - // ))?; - // - // match chain { - // Value::Array(v) => v, - // _ => { - // // the certificate chain should always be an object with just an - // // array of JWTs called "chain" - // return Err(ProtoCodecError::FormatMismatch( - // "Expected chain in JWT certificate_chain to be of type Array", - // )); - // } - // } - // } - // _ => { - // // the certificate chain should always be an object with just an array of - // // JWTs called "chain" - // return Err(ProtoCodecError::FormatMismatch( - // "Expected Object in base of JWT certificate_chain", - // )); - // } - // }; - // - // let mut key_data = vec![]; - // - // for jwt_json in certificate_chain_json_jwts { - // let jwt_string = match jwt_json { - // Value::String(str) => str, - // _ => { - // // the certificate chain's should always be a jwt string - // return Err(ProtoCodecError::FormatMismatch( - // "Expected chain array in certificate_chain to just contain Strings", - // )); - // } - // }; - // - // // Extract header - // let jwt_header = - // jsonwebtoken::decode_header(jwt_string).map_err(ProtoCodecError::JwtError)?; - // - // let mut jwt_validation = Validation::new(jwt_header.alg); - // // TODO: This definitely is not right. Even Zuri-MC doesn't understand this.. I may understand it.. I do understand it, update I don't. But I now know someone that does, I hope - // // TODO: Someone else should find out how this works - // jwt_validation.insecure_disable_signature_validation(); - // jwt_validation.set_required_spec_claims::<&str>(&[]); - // - // // Is first jwt, use self-signed header from x5u - // if key_data.is_empty() { - // let x5u = jwt_header.x5u.ok_or(ProtoCodecError::FormatMismatch( - // "Expected x5u in JWT header", - // ))?; - // - // let x5u = x5u.as_bytes(); - // - // key_data = BASE64_STANDARD - // .decode(x5u) - // .map_err(ProtoCodecError::Base64DecodeError)?; - // } - // - // // Decode the jwt string into a jwt object - // let jwt = jsonwebtoken::decode::>( - // &jwt_string, - // &DecodingKey::from_ec_der(&key_data), - // &jwt_validation, - // ) - // .map_err(ProtoCodecError::JwtError)?; - // - // let identity_field = - // jwt.claims - // .get("identityPublicKey") - // .ok_or(ProtoCodecError::FormatMismatch( - // "Missing identityPublicKey field in JWT for validation", - // ))?; - // - // key_data = match identity_field { - // Value::String(str) => BASE64_STANDARD - // .decode(str.as_bytes()) - // .map_err(ProtoCodecError::Base64DecodeError)?, - // _ => { - // return Err(ProtoCodecError::FormatMismatch( - // "Expected identityPublicKey field in JWT to be of type String", - // )) - // } - // }; - // - // certificate_chain.push(jwt.claims); - // } - // - // let raw_token_string = read_i32_string(stream)?; - // - // // Extract header - // let raw_token_jwt_header = - // jsonwebtoken::decode_header(&raw_token_string).map_err(ProtoCodecError::JwtError)?; - // - // let mut jwt_validation = Validation::new(raw_token_jwt_header.alg); - // // TODO: This definitely is not right. Even Zuri-MC doesn't understand this.. I may understand it.. I do understand it, update I don't. - // // TODO: Someone else should find out how this works - // jwt_validation.insecure_disable_signature_validation(); - // jwt_validation.set_required_spec_claims::<&str>(&[]); - // - // // Decode the jwt string into a jwt object - // let raw_token = jsonwebtoken::decode::>( - // &raw_token_string, - // &DecodingKey::from_ec_der(&[]), - // &jwt_validation, - // ) - // .map_err(ProtoCodecError::JwtError)? - // .claims; - // - // Ok(Self { - // certificate_chain, - // raw_token, - // }) } fn get_size_prediction(&self) -> usize { diff --git a/crates/proto_core/src/error.rs b/crates/proto_core/src/error.rs index c693bf2d..54422ba3 100644 --- a/crates/proto_core/src/error.rs +++ b/crates/proto_core/src/error.rs @@ -2,6 +2,7 @@ use std::convert::Infallible; use std::error::Error; use std::io::Error as IOError; use std::num::{ParseIntError, TryFromIntError}; +use std::str::Utf8Error; use std::string::FromUtf8Error; use base64::DecodeError as Base64DecodeError; @@ -20,7 +21,7 @@ pub enum ProtoCodecError { #[error("NbtError: {0}")] NbtError(#[from] NbtError), #[error("Error while reading UTF8 encoded String: {0}")] - UTF8Error(#[from] FromUtf8Error), + UTF8Error(#[from] Utf8Error), #[error("Error while converting integers: {0}")] FromIntError(#[from] TryFromIntError), #[error("Json Error: {0}")] diff --git a/crates/proto_core/src/types/string.rs b/crates/proto_core/src/types/string.rs index a69a3f57..81c123ee 100644 --- a/crates/proto_core/src/types/string.rs +++ b/crates/proto_core/src/types/string.rs @@ -27,7 +27,7 @@ impl ProtoCodec for String { let mut string_buf = vec![0u8; len]; stream.read_exact(&mut string_buf)?; - Ok(String::from_utf8(string_buf)?) + Ok(String::from_utf8(string_buf).map_err(|e| ProtoCodecError::UTF8Error(e.utf8_error()))?) } fn get_size_prediction(&self) -> usize { From fcd541b95be129babe84cb9fa2682c98c1773b45 Mon Sep 17 00:00:00 2001 From: = <44266876+RadiatedMonkey@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:02:45 +0100 Subject: [PATCH 04/11] add language code deserializer --- .../version/v729/types/connection_request.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index 768641a4..b46517b4 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -218,14 +218,27 @@ pub struct ClientInfo { pub device_model: String, #[serde(rename = "DeviceId")] pub device_id: String, - #[serde(rename = "LanguageCode")] - pub language_code: String, // TODO: Use LanguageCode enum instead + #[serde(rename = "LanguageCode", with = "language_code")] + pub language_code: LanguageCode, #[serde(rename = "UIProfile")] pub ui_profile: UIProfile, #[serde(rename = "GuiScale")] pub gui_scale: u32 } +mod language_code { + use bedrockrs_addon::language::code::LanguageCode; + use serde::{Deserialize, Deserializer}; + + #[inline] + pub fn deserialize<'de, D>(de: D) -> Result + where D: Deserializer<'de> + { + let lang = String::deserialize(de)?; + Ok(LanguageCode::VanillaCode(lang)) + } +} + #[derive(Deserialize, Debug)] struct UserData { #[serde(flatten)] From e25d9ada4962c143d6ae74bb26d1aa119e599ea5 Mon Sep 17 00:00:00 2001 From: = <44266876+RadiatedMonkey@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:36:07 +0100 Subject: [PATCH 05/11] add more fields to client info --- .../src/version/v662/enums/input_mode.rs | 3 +- .../version/v729/types/connection_request.rs | 240 ++++++++++-------- crates/proto_core/Cargo.toml | 1 + crates/proto_core/src/error.rs | 15 ++ 4 files changed, 154 insertions(+), 105 deletions(-) diff --git a/crates/proto/src/version/v662/enums/input_mode.rs b/crates/proto/src/version/v662/enums/input_mode.rs index 1461969a..cf9f402e 100644 --- a/crates/proto/src/version/v662/enums/input_mode.rs +++ b/crates/proto/src/version/v662/enums/input_mode.rs @@ -1,6 +1,7 @@ use bedrockrs_macros::ProtoCodec; +use serde_repr::Deserialize_repr; -#[derive(ProtoCodec, Clone, Debug)] +#[derive(ProtoCodec, Deserialize_repr, Clone, Debug)] #[enum_repr(u32)] #[enum_endianness(var)] #[repr(u32)] diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index b46517b4..b1d424a7 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -1,9 +1,10 @@ use std::collections::BTreeMap; use std::io::{Cursor, Read}; +use std::net::SocketAddr; use std::string::FromUtf8Error; use base64::prelude::BASE64_STANDARD; use base64::Engine; -use bedrockrs_proto_core::error::ProtoCodecError; +use bedrockrs_proto_core::error::{LoginError, ProtoCodecError}; use bedrockrs_proto_core::ProtoCodec; use byteorder::{LittleEndian, ReadBytesExt}; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; @@ -13,79 +14,84 @@ use varint_rs::VarintReader; use p384::pkcs8::spki; use uuid::Uuid; use bedrockrs_addon::language::code::LanguageCode; -use crate::v662::enums::{BuildPlatform, UIProfile}; -use crate::v662::types::SerializedSkin; +use crate::v662::enums::{BuildPlatform, InputMode, UIProfile}; +use crate::v662::types::{BaseGameVersion, SerializedSkin}; pub const MOJANG_PUBLIC_KEY: &str = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp"; #[derive(Debug, Clone)] pub struct ConnectionRequest { - /// Array of Base64 encoded JSON Web Token certificates to authenticate the player. - /// - /// The last certificate in the chain will have a property 'extraData' that contains player identity information including the XBL XUID (if the player was signed in to XBL at the time of the connection). - pub certificate_chain: Vec>, - /// Base64 encoded JSON Web Token that contains other relevant client properties. - /// - /// Properties Include: - /// - SelfSignedId - /// - ServerAddress = (unresolved url if applicable) - /// - ClientRandomId - /// - SkinId - /// - SkinData - /// - SkinImageWidth - /// - SkinImageHeight - /// - CapeData - /// - CapeImageWidth - /// - CapeImageHeight - /// - SkinResourcePatch - /// - SkinGeometryData - /// - SkinGeometryDataEngineVersion - /// - SkinAnimationData - /// - PlayFabId - /// - AnimatedImageData = Array of: - /// - Type - /// - Image - /// - ImageWidth - /// - ImageHeight - /// - Frames - /// - AnimationExpression - /// - ArmSize - /// - SkinColor - /// - PersonaPieces = Array of: - /// - PackId - /// - PieceId - /// - IsDefault - /// - PieceType - /// - ProductId - /// - PieceTintColors = Array of: - /// - PieceType - /// - Colors = Array of color hexstrings - /// - IsEduMode (if edu mode) - /// - TenantId (if edu mode) - /// - ADRole (if edu mode) - /// - IsEditorMode - /// - GameVersion - /// - DeviceModel - /// - DeviceOS = (see enumeration: BuildPlatform) - /// - DefaultInputMode = (see enumeration: InputMode) - /// - CurrentInputMode = (see enumeration: InputMode) - /// - UIProfile = (see enumeration: UIProfile) - /// - GuiScale - /// - LanguageCode - /// - PlatformUserId - /// - ThirdPartyName - /// - ThirdPartyNameOnly - /// - PlatformOnlineId - /// - PlatformOfflineId - /// - DeviceId - /// - TrustedSkin - /// - PremiumSkin - /// - PersonaSkin - /// - OverrideSkin - /// - CapeOnClassicSkin - /// - CapeId - /// - CompatibleWithClientSideChunkGen - pub raw_token: BTreeMap, + // /// Array of Base64 encoded JSON Web Token certificates to authenticate the player. + // /// + // /// The last certificate in the chain will have a property 'extraData' that contains player identity information including the XBL XUID (if the player was signed in to XBL at the time of the connection). + // pub certificate_chain: Vec>, + // /// Base64 encoded JSON Web Token that contains other relevant client properties. + // /// + // /// Properties Include: + // /// - SelfSignedId + // /// - ServerAddress = (unresolved url if applicable) + // /// - ClientRandomId + // /// - SkinId + // /// - SkinData + // /// - SkinImageWidth + // /// - SkinImageHeight + // /// - CapeData + // /// - CapeImageWidth + // /// - CapeImageHeight + // /// - SkinResourcePatch + // /// - SkinGeometryData + // /// - SkinGeometryDataEngineVersion + // /// - SkinAnimationData + // /// - PlayFabId + // /// - AnimatedImageData = Array of: + // /// - Type + // /// - Image + // /// - ImageWidth + // /// - ImageHeight + // /// - Frames + // /// - AnimationExpression + // /// - ArmSize + // /// - SkinColor + // /// - PersonaPieces = Array of: + // /// - PackId + // /// - PieceId + // /// - IsDefault + // /// - PieceType + // /// - ProductId + // /// - PieceTintColors = Array of: + // /// - PieceType + // /// - Colors = Array of color hexstrings + // /// - IsEduMode (if edu mode) + // /// - TenantId (if edu mode) + // /// - ADRole (if edu mode) + // /// - IsEditorMode + // /// - GameVersion + // /// - DeviceModel + // /// - DeviceOS = (see enumeration: BuildPlatform) + // /// - DefaultInputMode = (see enumeration: InputMode) + // /// - CurrentInputMode = (see enumeration: InputMode) + // /// - UIProfile = (see enumeration: UIProfile) + // /// - GuiScale + // /// - LanguageCode + // /// - PlatformUserId + // /// - ThirdPartyName + // /// - ThirdPartyNameOnly + // /// - PlatformOnlineId + // /// - PlatformOfflineId + // /// - DeviceId + // /// - TrustedSkin + // /// - PremiumSkin + // /// - PersonaSkin + // /// - OverrideSkin + // /// - CapeOnClassicSkin + // /// - CapeId + // /// - CompatibleWithClientSideChunkGen + pub info: ClientInfo, + pub xuid: String, + pub uuid: Uuid, + pub display_name: String, + pub public_key: String + // pub skin: Skin // TODO: Skin } #[derive(Deserialize, Debug)] @@ -107,8 +113,7 @@ fn parse_first_token(token: &str) -> Result { let bytes = BASE64_STANDARD.decode(base64_x5u)?; let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - unimplemented!("{e:?}"); - ProtoCodecError::LeftOvers(0) + LoginError::InvalidPublicKey(e) })?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); @@ -124,8 +129,7 @@ fn parse_first_token(token: &str) -> Result { fn parse_mojang_token(token: &str) -> Result { let bytes = BASE64_STANDARD.decode(MOJANG_PUBLIC_KEY)?; let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - unimplemented!("{e:?}"); - ProtoCodecError::LeftOvers(0) + LoginError::InvalidPublicKey(e) })?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); @@ -159,8 +163,7 @@ struct IdentityPayload { fn parse_identity_token(token: &str, key: &str) -> Result { let bytes = BASE64_STANDARD.decode(key)?; let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - unimplemented!("{e:?}"); - ProtoCodecError::LeftOvers(0) + LoginError::InvalidPublicKey(e) })?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); @@ -186,44 +189,74 @@ fn parse_identity(stream: &mut Cursor<&[u8]>) -> Result { - todo!(); + return Err(LoginError::UserOffline.into()) }, // Authenticated with Microsoft services 3 => { // Verify the first token and use its public key for the next token. - let mut valid = parse_first_token(&cert_chain.chain[0])?; + let valid = parse_first_token(&cert_chain.chain[0])?; if !valid { // Login attempted using forged token chain. - todo!() + return Err(LoginError::NotSignedByMojang.into()) } let key = parse_mojang_token(&cert_chain.chain[1])?; identity = parse_identity_token(&cert_chain.chain[2], &key)?; }, // This should not happen... - _ => { - todo!() + len => { + return Err(LoginError::InvalidChainLength(len).into()) } } Ok(identity) } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct ClientInfo { - #[serde(rename = "DeviceOS")] - pub build_platform: BuildPlatform, - #[serde(rename = "DeviceModel")] - pub device_model: String, + #[serde(rename = "ClientRandomId")] + pub client_random_id: i64, + #[serde(rename = "CompatibleWithClientSideChunkGen")] + pub client_side_chunk_gen_compatible: bool, + #[serde(rename = "CurrentInputMode")] + pub current_input_mode: InputMode, + #[serde(rename = "DefaultInputMode")] + pub default_input_mode: InputMode, #[serde(rename = "DeviceId")] pub device_id: String, + #[serde(rename = "DeviceModel")] + pub device_model: String, + #[serde(rename = "DeviceOS")] + pub build_platform: BuildPlatform, + #[serde(rename = "GameVersion")] + pub game_version: String, + #[serde(rename = "GuiScale")] + pub gui_scale: i32, + #[serde(rename = "IsEditorMode")] + pub editor_mode: bool, #[serde(rename = "LanguageCode", with = "language_code")] pub language_code: LanguageCode, + #[serde(rename = "MaxViewDistance")] + pub max_view_distance: u32, + #[serde(rename = "MemoryTier")] + pub memory_tier: u8, + #[serde(rename = "PlatformOfflineId")] + pub platform_offline_id: String, + #[serde(rename = "PlatformOnlineId")] + pub platform_online_id: String, + #[serde(rename = "PlatformType")] + pub platform_type: u8, + #[serde(rename = "SelfSignedId")] + pub self_signed_id: Uuid, + #[serde(rename = "ServerAddress")] + pub server_address: SocketAddr, + #[serde(rename = "ThirdPartyName")] + pub third_party_name: String, + #[serde(rename = "ThirdPartyNameOnly")] + pub third_party_name_only: bool, #[serde(rename = "UIProfile")] pub ui_profile: UIProfile, - #[serde(rename = "GuiScale")] - pub gui_scale: u32 } mod language_code { @@ -239,19 +272,10 @@ mod language_code { } } -#[derive(Deserialize, Debug)] -struct UserData { - #[serde(flatten)] - pub info: ClientInfo, - // #[serde(flatten)] - // pub skin: SerializedSkin -} - -fn parse_user_data_token(token: &str, key: &str) -> Result { +fn parse_client_info_token(token: &str, key: &str) -> Result { let bytes = BASE64_STANDARD.decode(key)?; let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - unimplemented!("{e:?}"); - ProtoCodecError::LeftOvers(0) + LoginError::InvalidPublicKey(e) })?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); @@ -259,11 +283,12 @@ fn parse_user_data_token(token: &str, key: &str) -> Result, public_key: &str) -> Result { +fn parse_client_info(stream: &mut Cursor<&[u8]>, public_key: &str) -> Result { let token_len = stream.read_i32::()?; let mut token = Vec::with_capacity(token_len as usize); token.resize(token_len as usize, 0); @@ -271,7 +296,7 @@ fn parse_user_data(stream: &mut Cursor<&[u8]>, public_key: &str) -> Result usize { diff --git a/crates/proto_core/Cargo.toml b/crates/proto_core/Cargo.toml index 6d91dc93..cc52f7bf 100644 --- a/crates/proto_core/Cargo.toml +++ b/crates/proto_core/Cargo.toml @@ -22,3 +22,4 @@ serde_json = "1.0" jsonwebtoken = "9.3" base64 = "0.22" uuid = { version = "1.11", features = ["v4"] } +p384 = "0.13.0" \ No newline at end of file diff --git a/crates/proto_core/src/error.rs b/crates/proto_core/src/error.rs index 54422ba3..87be0b2c 100644 --- a/crates/proto_core/src/error.rs +++ b/crates/proto_core/src/error.rs @@ -8,6 +8,7 @@ use std::string::FromUtf8Error; use base64::DecodeError as Base64DecodeError; use jsonwebtoken::errors::Error as JwtError; use nbtx::NbtError; +use p384::pkcs8::spki; use serde_json::error::Error as JsonError; use thiserror::Error; use uuid::Error as UuidError; @@ -45,6 +46,8 @@ pub enum ProtoCodecError { CompressError(#[from] CompressionError), #[error("Encryption Error: {0}")] EncryptionError(#[from] EncryptionError), + #[error("Login error: {0}")] + LoginError(#[from] LoginError) } impl From for ProtoCodecError { @@ -70,3 +73,15 @@ pub enum EncryptionError { #[error("IO Error: {0}")] IOError(IOError), } + +#[derive(Error, Debug)] +pub enum LoginError { + #[error("Invalid chain length: {0}")] + InvalidChainLength(usize), + #[error("Authentication token not signed by Mojang")] + NotSignedByMojang, + #[error("User is not authenticated with Xbox services")] + UserOffline, + #[error("Invalid public key: {0}")] + InvalidPublicKey(spki::Error) +} From ecdb2914a3752251fa13e34e46c11f646d3457f2 Mon Sep 17 00:00:00 2001 From: = <44266876+RadiatedMonkey@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:38:12 +0100 Subject: [PATCH 06/11] add x5u error --- crates/proto/src/version/v729/types/connection_request.rs | 2 +- crates/proto_core/src/error.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index b1d424a7..c1a60b2d 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -108,7 +108,7 @@ struct KeyPayload { fn parse_first_token(token: &str) -> Result { let header = jsonwebtoken::decode_header(token)?; let Some(base64_x5u) = header.x5u else { - todo!(); + return Err(LoginError::MissingX5U.into()) }; let bytes = BASE64_STANDARD.decode(base64_x5u)?; diff --git a/crates/proto_core/src/error.rs b/crates/proto_core/src/error.rs index 87be0b2c..fd93d038 100644 --- a/crates/proto_core/src/error.rs +++ b/crates/proto_core/src/error.rs @@ -76,6 +76,8 @@ pub enum EncryptionError { #[derive(Error, Debug)] pub enum LoginError { + #[error("Missing X5U header in JWT")] + MissingX5U, #[error("Invalid chain length: {0}")] InvalidChainLength(usize), #[error("Authentication token not signed by Mojang")] From 1523bd7a392f2c01e9ea9d6a1ab8162d3cb3cbf5 Mon Sep 17 00:00:00 2001 From: = <44266876+RadiatedMonkey@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:41:49 +0100 Subject: [PATCH 07/11] remove debug logging --- crates/macros/src/lib.rs | 2 -- crates/proto/src/codec.rs | 4 ---- 2 files changed, 6 deletions(-) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index c148bdc6..c6dfdde7 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -262,8 +262,6 @@ pub fn gamepackets(input: proc_macro::TokenStream) -> proc_macro::TokenStream { if let Some(v) = value { quote! { <#v as ::bedrockrs_proto_core::GamePacket>::ID => { - println!("Decoding {}", stringify!(#name)); - match <#v as ::bedrockrs_proto_core::ProtoCodec>::proto_deserialize(stream) { Ok(pk) => GamePackets::#name(pk), Err(e) => return Err(e), diff --git a/crates/proto/src/codec.rs b/crates/proto/src/codec.rs index b3add841..2b429ab9 100644 --- a/crates/proto/src/codec.rs +++ b/crates/proto/src/codec.rs @@ -27,13 +27,9 @@ pub fn decode_gamepackets( ) -> Result, ProtoCodecError> { log::trace!("Decoding gamepackets"); - println!("decrypt"); gamepacket_stream = decrypt_gamepackets::(gamepacket_stream, encryption)?; - println!("decompress"); gamepacket_stream = decompress_gamepackets::(gamepacket_stream, compression)?; - println!("separate"); let gamepackets = separate_gamepackets::(gamepacket_stream)?; - println!("done!"); Ok(gamepackets) } From 5c0422e50fd231ee1a92f2791f68950f530aadbe Mon Sep 17 00:00:00 2001 From: = <44266876+RadiatedMonkey@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:44:40 +0100 Subject: [PATCH 08/11] fix UTF-8 capitalization --- crates/proto_core/src/error.rs | 4 ++-- crates/proto_core/src/types/string.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/proto_core/src/error.rs b/crates/proto_core/src/error.rs index fd93d038..029932b5 100644 --- a/crates/proto_core/src/error.rs +++ b/crates/proto_core/src/error.rs @@ -21,8 +21,8 @@ pub enum ProtoCodecError { LeftOvers(usize), #[error("NbtError: {0}")] NbtError(#[from] NbtError), - #[error("Error while reading UTF8 encoded String: {0}")] - UTF8Error(#[from] Utf8Error), + #[error("Error while reading UTF-8 encoded String: {0}")] + Utf8Error(#[from] Utf8Error), #[error("Error while converting integers: {0}")] FromIntError(#[from] TryFromIntError), #[error("Json Error: {0}")] diff --git a/crates/proto_core/src/types/string.rs b/crates/proto_core/src/types/string.rs index 81c123ee..4d1eef4d 100644 --- a/crates/proto_core/src/types/string.rs +++ b/crates/proto_core/src/types/string.rs @@ -27,7 +27,7 @@ impl ProtoCodec for String { let mut string_buf = vec![0u8; len]; stream.read_exact(&mut string_buf)?; - Ok(String::from_utf8(string_buf).map_err(|e| ProtoCodecError::UTF8Error(e.utf8_error()))?) + Ok(String::from_utf8(string_buf).map_err(|e| ProtoCodecError::Utf8Error(e.utf8_error()))?) } fn get_size_prediction(&self) -> usize { From 221035607a540dc530faab78e828ef1642676dc6 Mon Sep 17 00:00:00 2001 From: = <44266876+RadiatedMonkey@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:46:43 +0100 Subject: [PATCH 09/11] run cargo-fmt --- crates/proto/src/encryption.rs | 5 +- .../version/v729/types/connection_request.rs | 77 +++++++++---------- crates/proto_core/src/error.rs | 4 +- crates/proto_core/src/types/string.rs | 5 +- examples/proto/server.rs | 2 +- 5 files changed, 47 insertions(+), 46 deletions(-) diff --git a/crates/proto/src/encryption.rs b/crates/proto/src/encryption.rs index 9a6729f7..d08c4c90 100644 --- a/crates/proto/src/encryption.rs +++ b/crates/proto/src/encryption.rs @@ -11,7 +11,10 @@ pub struct Encryption { impl Encryption { pub fn new() -> Self { Self { - recv_counter: 0, send_counter: 0, buf: [0; 8], key: Vec::new() + recv_counter: 0, + send_counter: 0, + buf: [0; 8], + key: Vec::new(), } } diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index c1a60b2d..dcbe8542 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -1,21 +1,21 @@ -use std::collections::BTreeMap; -use std::io::{Cursor, Read}; -use std::net::SocketAddr; -use std::string::FromUtf8Error; +use crate::v662::enums::{BuildPlatform, InputMode, UIProfile}; +use crate::v662::types::{BaseGameVersion, SerializedSkin}; use base64::prelude::BASE64_STANDARD; use base64::Engine; +use bedrockrs_addon::language::code::LanguageCode; use bedrockrs_proto_core::error::{LoginError, ProtoCodecError}; use bedrockrs_proto_core::ProtoCodec; use byteorder::{LittleEndian, ReadBytesExt}; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; +use p384::pkcs8::spki; use serde::Deserialize; use serde_json::Value; -use varint_rs::VarintReader; -use p384::pkcs8::spki; +use std::collections::BTreeMap; +use std::io::{Cursor, Read}; +use std::net::SocketAddr; +use std::string::FromUtf8Error; use uuid::Uuid; -use bedrockrs_addon::language::code::LanguageCode; -use crate::v662::enums::{BuildPlatform, InputMode, UIProfile}; -use crate::v662::types::{BaseGameVersion, SerializedSkin}; +use varint_rs::VarintReader; pub const MOJANG_PUBLIC_KEY: &str = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp"; @@ -90,31 +90,29 @@ pub struct ConnectionRequest { pub xuid: String, pub uuid: Uuid, pub display_name: String, - pub public_key: String - // pub skin: Skin // TODO: Skin + pub public_key: String, // pub skin: Skin // TODO: Skin } #[derive(Deserialize, Debug)] struct CertChain { - pub chain: Vec + pub chain: Vec, } #[derive(Deserialize, Debug)] struct KeyPayload { #[serde(rename = "identityPublicKey")] - pub public_key: String + pub public_key: String, } fn parse_first_token(token: &str) -> Result { let header = jsonwebtoken::decode_header(token)?; let Some(base64_x5u) = header.x5u else { - return Err(LoginError::MissingX5U.into()) + return Err(LoginError::MissingX5U.into()); }; let bytes = BASE64_STANDARD.decode(base64_x5u)?; - let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - LoginError::InvalidPublicKey(e) - })?; + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()) + .map_err(|e| LoginError::InvalidPublicKey(e))?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); let mut validation = Validation::new(Algorithm::ES384); @@ -128,9 +126,8 @@ fn parse_first_token(token: &str) -> Result { fn parse_mojang_token(token: &str) -> Result { let bytes = BASE64_STANDARD.decode(MOJANG_PUBLIC_KEY)?; - let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - LoginError::InvalidPublicKey(e) - })?; + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()) + .map_err(|e| LoginError::InvalidPublicKey(e))?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); let mut validation = Validation::new(Algorithm::ES384); @@ -149,7 +146,7 @@ pub struct RawIdentity { #[serde(rename = "displayName")] pub display_name: String, #[serde(rename = "identity")] - pub uuid: Uuid + pub uuid: Uuid, } #[derive(Deserialize, Debug)] @@ -157,14 +154,13 @@ struct IdentityPayload { #[serde(rename = "extraData")] pub client_data: RawIdentity, #[serde(rename = "identityPublicKey")] - pub public_key: String + pub public_key: String, } fn parse_identity_token(token: &str, key: &str) -> Result { let bytes = BASE64_STANDARD.decode(key)?; - let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - LoginError::InvalidPublicKey(e) - })?; + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()) + .map_err(|e| LoginError::InvalidPublicKey(e))?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); let mut validation = Validation::new(Algorithm::ES384); @@ -188,9 +184,7 @@ fn parse_identity(stream: &mut Cursor<&[u8]>) -> Result { - return Err(LoginError::UserOffline.into()) - }, + 1 => return Err(LoginError::UserOffline.into()), // Authenticated with Microsoft services 3 => { // Verify the first token and use its public key for the next token. @@ -198,16 +192,14 @@ fn parse_identity(stream: &mut Cursor<&[u8]>) -> Result { - return Err(LoginError::InvalidChainLength(len).into()) } + // This should not happen... + len => return Err(LoginError::InvalidChainLength(len).into()), } Ok(identity) @@ -262,10 +254,11 @@ pub struct ClientInfo { mod language_code { use bedrockrs_addon::language::code::LanguageCode; use serde::{Deserialize, Deserializer}; - + #[inline] - pub fn deserialize<'de, D>(de: D) -> Result - where D: Deserializer<'de> + pub fn deserialize<'de, D>(de: D) -> Result + where + D: Deserializer<'de>, { let lang = String::deserialize(de)?; Ok(LanguageCode::VanillaCode(lang)) @@ -274,9 +267,8 @@ mod language_code { fn parse_client_info_token(token: &str, key: &str) -> Result { let bytes = BASE64_STANDARD.decode(key)?; - let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()).map_err(|e| { - LoginError::InvalidPublicKey(e) - })?; + let public_key = spki::SubjectPublicKeyInfoRef::try_from(bytes.as_ref()) + .map_err(|e| LoginError::InvalidPublicKey(e))?; let decoding_key = DecodingKey::from_ec_der(public_key.subject_public_key.raw_bytes()); let mut validation = Validation::new(Algorithm::ES384); @@ -288,7 +280,10 @@ fn parse_client_info_token(token: &str, key: &str) -> Result, public_key: &str) -> Result { +fn parse_client_info( + stream: &mut Cursor<&[u8]>, + public_key: &str, +) -> Result { let token_len = stream.read_i32::()?; let mut token = Vec::with_capacity(token_len as usize); token.resize(token_len as usize, 0); @@ -323,7 +318,7 @@ impl ProtoCodec for ConnectionRequest { xuid: identity.client_data.xuid, uuid: identity.client_data.uuid, info: user_data, - public_key: identity.public_key + public_key: identity.public_key, }; dbg!(&login); diff --git a/crates/proto_core/src/error.rs b/crates/proto_core/src/error.rs index 029932b5..0f14e6d2 100644 --- a/crates/proto_core/src/error.rs +++ b/crates/proto_core/src/error.rs @@ -47,7 +47,7 @@ pub enum ProtoCodecError { #[error("Encryption Error: {0}")] EncryptionError(#[from] EncryptionError), #[error("Login error: {0}")] - LoginError(#[from] LoginError) + LoginError(#[from] LoginError), } impl From for ProtoCodecError { @@ -85,5 +85,5 @@ pub enum LoginError { #[error("User is not authenticated with Xbox services")] UserOffline, #[error("Invalid public key: {0}")] - InvalidPublicKey(spki::Error) + InvalidPublicKey(spki::Error), } diff --git a/crates/proto_core/src/types/string.rs b/crates/proto_core/src/types/string.rs index 4d1eef4d..3ce65891 100644 --- a/crates/proto_core/src/types/string.rs +++ b/crates/proto_core/src/types/string.rs @@ -27,7 +27,10 @@ impl ProtoCodec for String { let mut string_buf = vec![0u8; len]; stream.read_exact(&mut string_buf)?; - Ok(String::from_utf8(string_buf).map_err(|e| ProtoCodecError::Utf8Error(e.utf8_error()))?) + Ok( + String::from_utf8(string_buf) + .map_err(|e| ProtoCodecError::Utf8Error(e.utf8_error()))?, + ) } fn get_size_prediction(&self) -> usize { diff --git a/examples/proto/server.rs b/examples/proto/server.rs index d5b15637..8c1d321f 100644 --- a/examples/proto/server.rs +++ b/examples/proto/server.rs @@ -8,8 +8,8 @@ use bedrockrs_proto::v662::packets::{ use bedrockrs_proto::v662::types::{BaseGameVersion, Experiments}; use bedrockrs_proto::v662::GamePackets; use bedrockrs_proto::v662::ProtoHelperV662; -use tokio::time::Instant; use bedrockrs_proto::v729::helper::ProtoHelperV729; +use tokio::time::Instant; #[tokio::main] async fn main() { From 8b822b8e653af17566b8e4ab7e81c86e18a4489e Mon Sep 17 00:00:00 2001 From: Ruben Adema <44266876+RadiatedMonkey@users.noreply.github.com> Date: Fri, 3 Jan 2025 00:02:49 +0100 Subject: [PATCH 10/11] add non-functional encryption --- crates/proto/Cargo.toml | 3 + crates/proto/src/codec.rs | 4 +- crates/proto/src/encryption.rs | 157 ++++++++++++++++-- .../packets/handshake_server_to_client.rs | 19 ++- .../version/v729/types/connection_request.rs | 3 - examples/proto/server.rs | 102 +++++++----- 6 files changed, 222 insertions(+), 66 deletions(-) diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 756996cd..265ec5c9 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -26,6 +26,9 @@ uuid = { version = "1.11", features = ["v4"] } serde_json = "1.0" tokio = { version = "1.40", features = ["full"] } p384 = "0.13.0" +sha2 = "0.10.8" +aes = "0.8.4" +ctr = "0.9.2" flate2 = "1.0" snap = "1.1" diff --git a/crates/proto/src/codec.rs b/crates/proto/src/codec.rs index 2b429ab9..4c2ec0d3 100644 --- a/crates/proto/src/codec.rs +++ b/crates/proto/src/codec.rs @@ -101,7 +101,7 @@ pub fn encrypt_gamepackets( encryption: Option<&mut Encryption>, ) -> Result, ProtoCodecError> { if let Some(encryption) = encryption { - gamepacket_stream = encryption.encrypt(gamepacket_stream)?; + encryption.encrypt(&mut gamepacket_stream)?; } Ok(gamepacket_stream) @@ -114,7 +114,7 @@ pub fn decrypt_gamepackets( dbg!("Attempting to decrypt"); if let Some(encryption) = encryption { - gamepacket_stream = encryption.decrypt(gamepacket_stream)?; + encryption.decrypt(&mut gamepacket_stream)?; } Ok(gamepacket_stream) diff --git a/crates/proto/src/encryption.rs b/crates/proto/src/encryption.rs index d08c4c90..ef792448 100644 --- a/crates/proto/src/encryption.rs +++ b/crates/proto/src/encryption.rs @@ -1,32 +1,157 @@ -use bedrockrs_proto_core::error::EncryptionError; +use std::fmt::{Debug, self}; +use std::io::Write; +use base64::Engine; +use base64::prelude::BASE64_STANDARD; +use jsonwebtoken::Algorithm; +use p384::ecdh::diffie_hellman; +use p384::ecdsa::SigningKey; +use p384::pkcs8::{DecodePublicKey, EncodePrivateKey, EncodePublicKey}; +use p384::PublicKey; +use rand::distributions::Alphanumeric; +use rand::Rng; +use rand::rngs::OsRng; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use ctr::cipher::{KeyIvInit, StreamCipher}; +use bedrockrs_proto_core::error::{EncryptionError, ProtoCodecError}; -#[derive(Debug, Clone)] +type Aes256CtrBE = ctr::Ctr64BE; + +#[derive(Serialize)] +struct EncryptionClaims { + salt: String +} + +#[derive(Clone)] pub struct Encryption { recv_counter: u64, send_counter: u64, - buf: [u8; 8], - key: Vec, + jwt: String, + + decrypt: Aes256CtrBE, + encrypt: Aes256CtrBE, + secret: [u8; 32] +} + +impl Debug for Encryption { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("Encryption") + .field("jwt", &self.jwt) + .finish_non_exhaustive() + } } impl Encryption { - pub fn new() -> Self { - Self { - recv_counter: 0, + pub fn new(client_public_key_der: &str) -> Result { + let salt = (0..16).map(|_| OsRng.sample(Alphanumeric) as char).collect::(); + let private_key: SigningKey = SigningKey::random(&mut OsRng); + + let Ok(private_key_der) = private_key.to_pkcs8_der() else { + todo!() + }; + + let public_key = private_key.verifying_key(); + let public_key_der = if let Ok(key) = public_key.to_public_key_der() { + BASE64_STANDARD.encode(key) + } else { + todo!() + }; + + let mut header = jsonwebtoken::Header::new(Algorithm::ES384); + header.typ = None; + header.x5u = Some(public_key_der); + + let signing_key = jsonwebtoken::EncodingKey::from_ec_der(&private_key_der.to_bytes()); + let claims = EncryptionClaims { salt: BASE64_STANDARD.encode(&salt) }; + + let jwt = jsonwebtoken::encode(&header, &claims, &signing_key)?; + + let bytes = BASE64_STANDARD.decode(client_public_key_der)?; + let Ok(client_public_key) = PublicKey::from_public_key_der(&bytes) else { + todo!() + }; + + let shared_secret = diffie_hellman( + private_key.as_nonzero_scalar(), client_public_key.as_affine() + ); + + let mut hasher = Sha256::new(); + hasher.update(salt); + hasher.update(shared_secret.raw_secret_bytes().as_slice()); + + let mut secret = [0; 32]; + secret.copy_from_slice(&hasher.finalize()[..32]); + + let mut iv = [0; 16]; + iv[..12].copy_from_slice(&secret[..12]); + iv[12..].copy_from_slice(&[0x00, 0x00, 0x00, 0x02]); + + let cipher = Aes256CtrBE::new((&secret).into(), (&iv).into()); + + Ok(Self { send_counter: 0, - buf: [0; 8], - key: Vec::new(), - } + recv_counter: 0, + + decrypt: cipher.clone(), + encrypt: cipher, + secret, + + jwt + }) + } + + #[inline] + pub fn salt_jwt(&self) -> &str { + &self.jwt } - pub fn decrypt(&mut self, _src: Vec) -> Result, EncryptionError> { - unimplemented!() + pub fn decrypt(&mut self, data: &mut Vec) -> Result<(), ProtoCodecError> { + dbg!("Decrypting"); + + if data.len() < 9 { + // This data cannot possibly be valid. Checksum is already 8 bytes. + todo!() + } + + self.decrypt.apply_keystream(data); + + let counter = self.recv_counter; + self.recv_counter += 1; + + let checksum = &data[data.len() - 8..]; + let computed = self.checksum(&data[..data.len() - 8], counter); + + if !checksum.eq(&computed) { + todo!() + } + + data.truncate(data.len() - 8); + Ok(()) } - pub fn encrypt(&mut self, _src: Vec) -> Result, EncryptionError> { - unimplemented!() + pub fn encrypt(&mut self, data: &mut Vec) -> Result<(), ProtoCodecError> { + println!("{data:?} {}", String::from_utf8_lossy(data)); + + let counter = self.send_counter; + self.send_counter += 1; + + let checksum = self.checksum(&data, counter); + data.write_all(&checksum)?; + + self.encrypt.apply_keystream(data); + + Ok(()) } - pub fn verify(&mut self, _src: &[u8]) -> Result<(), EncryptionError> { - unimplemented!() + pub fn checksum(&self, data: &[u8], counter: u64) -> [u8; 8] { + let mut hasher = Sha256::new(); + hasher.update(counter.to_le_bytes()); + hasher.update(data); + hasher.update(&self.secret); + + let mut checksum = [0; 8]; + checksum.copy_from_slice(&hasher.finalize()[..8]); + + checksum } } diff --git a/crates/proto/src/version/v729/packets/handshake_server_to_client.rs b/crates/proto/src/version/v729/packets/handshake_server_to_client.rs index 7eb3a90a..d1a5edcc 100644 --- a/crates/proto/src/version/v729/packets/handshake_server_to_client.rs +++ b/crates/proto/src/version/v729/packets/handshake_server_to_client.rs @@ -1,30 +1,33 @@ use std::collections::BTreeMap; -use std::io::Cursor; +use std::io::{Cursor, Write}; -use bedrockrs_macros::gamepacket; +use bedrockrs_macros::{gamepacket, ProtoCodec}; use bedrockrs_proto_core::error::ProtoCodecError; use bedrockrs_proto_core::ProtoCodec; use serde_json::Value; - +use varint_rs::VarintWriter; // Yeah we aren't supporting secure things rn... #[gamepacket(id = 3)] #[derive(Debug, Clone)] pub struct HandshakeServerToClientPacket { - pub handshake_jwt: BTreeMap, + pub jwt: String } impl ProtoCodec for HandshakeServerToClientPacket { - fn proto_serialize(&self, _stream: &mut Vec) -> Result<(), ProtoCodecError> { - todo!() + fn proto_serialize(&self, stream: &mut Vec) -> Result<(), ProtoCodecError> { + stream.write_u32_varint(self.jwt.len() as u32)?; + stream.write_all(self.jwt.as_bytes())?; + + Ok(()) } - fn proto_deserialize(_stream: &mut Cursor<&[u8]>) -> Result { + fn proto_deserialize(stream: &mut Cursor<&[u8]>) -> Result { todo!() } fn get_size_prediction(&self) -> usize { - todo!() + 300 } } diff --git a/crates/proto/src/version/v729/types/connection_request.rs b/crates/proto/src/version/v729/types/connection_request.rs index dcbe8542..35c8cc1d 100644 --- a/crates/proto/src/version/v729/types/connection_request.rs +++ b/crates/proto/src/version/v729/types/connection_request.rs @@ -275,7 +275,6 @@ fn parse_client_info_token(token: &str, key: &str) -> Result) -> Result where Self: Sized, diff --git a/examples/proto/server.rs b/examples/proto/server.rs index 8c1d321f..91ec781f 100644 --- a/examples/proto/server.rs +++ b/examples/proto/server.rs @@ -6,10 +6,13 @@ use bedrockrs_proto::v662::packets::{ NetworkSettingsPacket, PlayStatusPacket, ResourcePackStackPacket, ResourcePacksInfoPacket, }; use bedrockrs_proto::v662::types::{BaseGameVersion, Experiments}; -use bedrockrs_proto::v662::GamePackets; +use bedrockrs_proto::v662::GamePackets as GamePackets662; +use bedrockrs_proto::v729::gamepackets::GamePackets as GamePackets729; use bedrockrs_proto::v662::ProtoHelperV662; use bedrockrs_proto::v729::helper::ProtoHelperV729; use tokio::time::Instant; +use bedrockrs_proto::encryption::Encryption; +use bedrockrs_proto::v729::packets::handshake_server_to_client::HandshakeServerToClientPacket; #[tokio::main] async fn main() { @@ -46,7 +49,7 @@ async fn handle_login(mut conn: Connection) { let compression = Compression::None; // NetworkSettings - conn.send::(&[GamePackets::NetworkSettings(NetworkSettingsPacket { + conn.send::(&[GamePackets662::NetworkSettings(NetworkSettingsPacket { compression_threshold: 1, compression_algorithm: PacketCompressionAlgorithm::None, client_throttle_enabled: false, @@ -60,43 +63,68 @@ async fn handle_login(mut conn: Connection) { conn.compression = Some(compression); // Login - conn.recv::().await.unwrap(); + let packets = conn.recv::().await.unwrap(); + let public_key = { + let first = packets.first().unwrap(); + let GamePackets729::Login(login) = first else { + unreachable!(); + }; + + login.connection_request.public_key.clone() + }; + + let encryptor = Encryption::new(&public_key).unwrap(); + let jwt = encryptor.salt_jwt().to_owned(); + conn.encryption = Some(encryptor); + + conn.send::(&[ + GamePackets729::HandshakeServerToClient(HandshakeServerToClientPacket { + jwt + }) + ]).await.unwrap(); + + let recv = conn.recv::().await.unwrap(); + dbg!(recv); + println!("Login"); + dbg!(packets); - conn.send::(&[ - GamePackets::PlaySatus(PlayStatusPacket { - status: PlayStatus::LoginSuccess, - }), - GamePackets::ResourcePacksInfo(ResourcePacksInfoPacket { - resource_pack_required: false, - has_addon_packs: false, - has_scripts: false, - force_server_packs_enabled: false, - behaviour_packs: vec![], - resource_packs: vec![], - cdn_urls: vec![], - }), - GamePackets::ResourcePackStack(ResourcePackStackPacket { - texture_pack_required: false, - addon_list: vec![], - base_game_version: BaseGameVersion(String::from("1.0")), - experiments: Experiments { - experiments: vec![], - ever_toggled: false, - }, - texture_pack_list: vec![], - }), - ]) - .await - .unwrap(); - println!("PlayStatus (LoginSuccess)"); - println!("ResourcePacksInfo"); - println!("ResourcePackStack"); - - println!("{:#?}", conn.recv::().await.unwrap()); - println!("ClientCacheStatus"); - println!("{:#?}", conn.recv::().await.unwrap()); - println!("ResourcePackClientResponse"); + todo!(); + + // conn.send::(&[ + // GamePackets::PlaySatus(PlayStatusPacket { + // status: PlayStatus::LoginSuccess, + // }), + // GamePackets::ResourcePacksInfo(ResourcePacksInfoPacket { + // resource_pack_required: false, + // has_addon_packs: false, + // has_scripts: false, + // force_server_packs_enabled: false, + // behaviour_packs: vec![], + // resource_packs: vec![], + // cdn_urls: vec![], + // }), + // GamePackets::ResourcePackStack(ResourcePackStackPacket { + // texture_pack_required: false, + // addon_list: vec![], + // base_game_version: BaseGameVersion(String::from("1.0")), + // experiments: Experiments { + // experiments: vec![], + // ever_toggled: false, + // }, + // texture_pack_list: vec![], + // }), + // ]) + // .await + // .unwrap(); + // println!("PlayStatus (LoginSuccess)"); + // println!("ResourcePacksInfo"); + // println!("ResourcePackStack"); + // + // println!("{:#?}", conn.recv::().await.unwrap()); + // println!("ClientCacheStatus"); + // println!("{:#?}", conn.recv::().await.unwrap()); + // println!("ResourcePackClientResponse"); // conn.send::(&[GamePackets::DisconnectPlayer(DisconnectPlayerPacket { // reason: DisconnectReason::Unknown, From 8085a9a46a031081022c17f9699595b058903d0f Mon Sep 17 00:00:00 2001 From: = <44266876+RadiatedMonkey@users.noreply.github.com> Date: Sat, 4 Jan 2025 02:21:37 +0100 Subject: [PATCH 11/11] encryption works now (apart from batched packets?) --- crates/proto/src/codec.rs | 2 - crates/proto/src/encryption.rs | 44 +++++---- crates/proto/src/info.rs | 4 +- .../enums/packet_compression_algorithm.rs | 2 +- .../packets/handshake_server_to_client.rs | 2 +- examples/proto/server.rs | 91 ++++++++++--------- 6 files changed, 75 insertions(+), 70 deletions(-) diff --git a/crates/proto/src/codec.rs b/crates/proto/src/codec.rs index 4c2ec0d3..a2b72fc9 100644 --- a/crates/proto/src/codec.rs +++ b/crates/proto/src/codec.rs @@ -111,8 +111,6 @@ pub fn decrypt_gamepackets( mut gamepacket_stream: Vec, encryption: Option<&mut Encryption>, ) -> Result, ProtoCodecError> { - dbg!("Attempting to decrypt"); - if let Some(encryption) = encryption { encryption.decrypt(&mut gamepacket_stream)?; } diff --git a/crates/proto/src/encryption.rs b/crates/proto/src/encryption.rs index ef792448..22c4f304 100644 --- a/crates/proto/src/encryption.rs +++ b/crates/proto/src/encryption.rs @@ -1,25 +1,25 @@ -use std::fmt::{Debug, self}; -use std::io::Write; -use base64::Engine; use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use bedrockrs_proto_core::error::{EncryptionError, ProtoCodecError}; +use ctr::cipher::{KeyIvInit, StreamCipher}; use jsonwebtoken::Algorithm; use p384::ecdh::diffie_hellman; use p384::ecdsa::SigningKey; use p384::pkcs8::{DecodePublicKey, EncodePrivateKey, EncodePublicKey}; use p384::PublicKey; use rand::distributions::Alphanumeric; -use rand::Rng; use rand::rngs::OsRng; +use rand::Rng; use serde::Serialize; use sha2::{Digest, Sha256}; -use ctr::cipher::{KeyIvInit, StreamCipher}; -use bedrockrs_proto_core::error::{EncryptionError, ProtoCodecError}; +use std::fmt::{self, Debug}; +use std::io::Write; type Aes256CtrBE = ctr::Ctr64BE; #[derive(Serialize)] struct EncryptionClaims { - salt: String + salt: String, } #[derive(Clone)] @@ -30,7 +30,7 @@ pub struct Encryption { decrypt: Aes256CtrBE, encrypt: Aes256CtrBE, - secret: [u8; 32] + secret: [u8; 32], } impl Debug for Encryption { @@ -43,7 +43,9 @@ impl Debug for Encryption { impl Encryption { pub fn new(client_public_key_der: &str) -> Result { - let salt = (0..16).map(|_| OsRng.sample(Alphanumeric) as char).collect::(); + let salt = (0..16) + .map(|_| OsRng.sample(Alphanumeric) as char) + .collect::(); let private_key: SigningKey = SigningKey::random(&mut OsRng); let Ok(private_key_der) = private_key.to_pkcs8_der() else { @@ -62,7 +64,9 @@ impl Encryption { header.x5u = Some(public_key_der); let signing_key = jsonwebtoken::EncodingKey::from_ec_der(&private_key_der.to_bytes()); - let claims = EncryptionClaims { salt: BASE64_STANDARD.encode(&salt) }; + let claims = EncryptionClaims { + salt: BASE64_STANDARD.encode(&salt), + }; let jwt = jsonwebtoken::encode(&header, &claims, &signing_key)?; @@ -72,15 +76,15 @@ impl Encryption { }; let shared_secret = diffie_hellman( - private_key.as_nonzero_scalar(), client_public_key.as_affine() + private_key.as_nonzero_scalar(), + client_public_key.as_affine(), ); let mut hasher = Sha256::new(); hasher.update(salt); hasher.update(shared_secret.raw_secret_bytes().as_slice()); - let mut secret = [0; 32]; - secret.copy_from_slice(&hasher.finalize()[..32]); + let secret = hasher.finalize(); let mut iv = [0; 16]; iv[..12].copy_from_slice(&secret[..12]); @@ -94,9 +98,9 @@ impl Encryption { decrypt: cipher.clone(), encrypt: cipher, - secret, + secret: secret.into(), - jwt + jwt, }) } @@ -107,6 +111,7 @@ impl Encryption { pub fn decrypt(&mut self, data: &mut Vec) -> Result<(), ProtoCodecError> { dbg!("Decrypting"); + println!("data: {:?}", &data[..10]); if data.len() < 9 { // This data cannot possibly be valid. Checksum is already 8 bytes. @@ -114,28 +119,27 @@ impl Encryption { } self.decrypt.apply_keystream(data); + println!("decrypt: {data:?}"); let counter = self.recv_counter; self.recv_counter += 1; let checksum = &data[data.len() - 8..]; let computed = self.checksum(&data[..data.len() - 8], counter); - if !checksum.eq(&computed) { - todo!() + panic!("Checksum does not match") } data.truncate(data.len() - 8); + Ok(()) } pub fn encrypt(&mut self, data: &mut Vec) -> Result<(), ProtoCodecError> { - println!("{data:?} {}", String::from_utf8_lossy(data)); - let counter = self.send_counter; self.send_counter += 1; - let checksum = self.checksum(&data, counter); + let checksum = self.checksum(data, counter); data.write_all(&checksum)?; self.encrypt.apply_keystream(data); diff --git a/crates/proto/src/info.rs b/crates/proto/src/info.rs index 6913a831..cb8913e6 100644 --- a/crates/proto/src/info.rs +++ b/crates/proto/src/info.rs @@ -5,5 +5,5 @@ pub const MAGIC: [u8; 16] = [ 0x00, 0xff, 0xff, 0x0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78, ]; -/// Mojang's public JWT Key encoded as a base64 str -pub const MOAJNG_PUBLIC_KEY: &'static str = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp"; +/// Mojang's public JWT Key encoded in DER format. +pub const MOJANG_PUBLIC_KEY: &str = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp"; diff --git a/crates/proto/src/version/v662/enums/packet_compression_algorithm.rs b/crates/proto/src/version/v662/enums/packet_compression_algorithm.rs index 72e61abc..0104d6e8 100644 --- a/crates/proto/src/version/v662/enums/packet_compression_algorithm.rs +++ b/crates/proto/src/version/v662/enums/packet_compression_algorithm.rs @@ -5,7 +5,7 @@ use bedrockrs_macros::ProtoCodec; #[enum_endianness(le)] #[repr(u16)] pub enum PacketCompressionAlgorithm { - ZLib = 0, + Zlib = 0, Snappy = 1, None = 0xffff, } \ No newline at end of file diff --git a/crates/proto/src/version/v729/packets/handshake_server_to_client.rs b/crates/proto/src/version/v729/packets/handshake_server_to_client.rs index d1a5edcc..3fddb556 100644 --- a/crates/proto/src/version/v729/packets/handshake_server_to_client.rs +++ b/crates/proto/src/version/v729/packets/handshake_server_to_client.rs @@ -11,7 +11,7 @@ use varint_rs::VarintWriter; #[gamepacket(id = 3)] #[derive(Debug, Clone)] pub struct HandshakeServerToClientPacket { - pub jwt: String + pub jwt: String, } impl ProtoCodec for HandshakeServerToClientPacket { diff --git a/examples/proto/server.rs b/examples/proto/server.rs index 91ec781f..c349c36d 100644 --- a/examples/proto/server.rs +++ b/examples/proto/server.rs @@ -1,18 +1,18 @@ use bedrockrs::proto::connection::Connection; use bedrockrs::proto::listener::Listener; use bedrockrs_proto::compression::Compression; +use bedrockrs_proto::encryption::Encryption; use bedrockrs_proto::v662::enums::{PacketCompressionAlgorithm, PlayStatus}; use bedrockrs_proto::v662::packets::{ NetworkSettingsPacket, PlayStatusPacket, ResourcePackStackPacket, ResourcePacksInfoPacket, }; use bedrockrs_proto::v662::types::{BaseGameVersion, Experiments}; use bedrockrs_proto::v662::GamePackets as GamePackets662; -use bedrockrs_proto::v729::gamepackets::GamePackets as GamePackets729; use bedrockrs_proto::v662::ProtoHelperV662; +use bedrockrs_proto::v729::gamepackets::GamePackets as GamePackets729; use bedrockrs_proto::v729::helper::ProtoHelperV729; -use tokio::time::Instant; -use bedrockrs_proto::encryption::Encryption; use bedrockrs_proto::v729::packets::handshake_server_to_client::HandshakeServerToClientPacket; +use tokio::time::Instant; #[tokio::main] async fn main() { @@ -46,12 +46,15 @@ async fn handle_login(mut conn: Connection) { conn.recv::().await.unwrap(); println!("NetworkSettingsRequest"); - let compression = Compression::None; + let compression = Compression::Zlib { + threshold: 1, + compression_level: 6, + }; // NetworkSettings conn.send::(&[GamePackets662::NetworkSettings(NetworkSettingsPacket { compression_threshold: 1, - compression_algorithm: PacketCompressionAlgorithm::None, + compression_algorithm: PacketCompressionAlgorithm::Zlib, client_throttle_enabled: false, client_throttle_threshold: 0, client_throttle_scalar: 0.0, @@ -75,52 +78,52 @@ async fn handle_login(mut conn: Connection) { let encryptor = Encryption::new(&public_key).unwrap(); let jwt = encryptor.salt_jwt().to_owned(); - conn.encryption = Some(encryptor); - conn.send::(&[ - GamePackets729::HandshakeServerToClient(HandshakeServerToClientPacket { - jwt - }) - ]).await.unwrap(); + conn.send::(&[GamePackets729::HandshakeServerToClient( + HandshakeServerToClientPacket { jwt }, + )]) + .await + .unwrap(); + println!("HandshakeServerToClient"); + + conn.encryption = Some(encryptor); let recv = conn.recv::().await.unwrap(); - dbg!(recv); - println!("Login"); dbg!(packets); - todo!(); - - // conn.send::(&[ - // GamePackets::PlaySatus(PlayStatusPacket { - // status: PlayStatus::LoginSuccess, - // }), - // GamePackets::ResourcePacksInfo(ResourcePacksInfoPacket { - // resource_pack_required: false, - // has_addon_packs: false, - // has_scripts: false, - // force_server_packs_enabled: false, - // behaviour_packs: vec![], - // resource_packs: vec![], - // cdn_urls: vec![], - // }), - // GamePackets::ResourcePackStack(ResourcePackStackPacket { - // texture_pack_required: false, - // addon_list: vec![], - // base_game_version: BaseGameVersion(String::from("1.0")), - // experiments: Experiments { - // experiments: vec![], - // ever_toggled: false, - // }, - // texture_pack_list: vec![], - // }), - // ]) - // .await - // .unwrap(); - // println!("PlayStatus (LoginSuccess)"); - // println!("ResourcePacksInfo"); + conn.send::(&[ + GamePackets662::PlaySatus(PlayStatusPacket { + status: PlayStatus::LoginSuccess, + }), // GamePackets662::ResourcePacksInfo(ResourcePacksInfoPacket { + // resource_pack_required: false, + // has_addon_packs: false, + // has_scripts: false, + // force_server_packs_enabled: false, + // behaviour_packs: vec![], + // resource_packs: vec![], + // cdn_urls: vec![], + // }) + // GamePackets662::ResourcePackStack(ResourcePackStackPacket { + // texture_pack_required: false, + // addon_list: vec![], + // base_game_version: BaseGameVersion(String::from("1.0")), + // experiments: Experiments { + // experiments: vec![], + // ever_toggled: false, + // }, + // texture_pack_list: vec![], + // }), + ]) + .await + .unwrap(); + println!("PlayStatus (LoginSuccess)"); + println!("ResourcePacksInfo"); // println!("ResourcePackStack"); - // + + let recv = conn.recv::().await.unwrap(); + dbg!(recv); + // println!("{:#?}", conn.recv::().await.unwrap()); // println!("ClientCacheStatus"); // println!("{:#?}", conn.recv::().await.unwrap());