diff --git a/Cargo.lock b/Cargo.lock index af446dfcd..dc2185c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ + "block-buffer 0.12.1", "crypto-common 0.2.2", "inout", ] @@ -964,6 +965,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -1111,6 +1121,7 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", @@ -1513,6 +1524,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2810,6 +2830,7 @@ dependencies = [ "bitflags", "bytes", "cfb8", + "ctr", "flate2", "hybrid-array", "pumpkin-data", @@ -2818,6 +2839,7 @@ dependencies = [ "pumpkin-util", "pumpkin-world", "serde", + "sha2 0.11.0", "thiserror 2.0.18", "tokio", "uuid", diff --git a/Cargo.toml b/Cargo.toml index ce28487af..a9a1e1465 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,7 @@ base64 = { version = "0.22.1", default-features = false, features = ["std"] } bitflags = { version = "2.13.0", default-features = false, features = ["std"] } cesu8 = { version = "1.1", default-features = false } cfb8 = { version = "0.9", default-features = false } +ctr = { version = "0.10.1", default-features = false } colored = { version = "3.1", default-features = false } console-subscriber = { version = "0.5.0", default-features = false } crc-fast = { version = "1.10.0", default-features = false, features = ["std"] } @@ -146,7 +147,7 @@ md5 = { version = "0.8", default-features = false } num-bigint = { version = "0.5", default-features = false, features = ["std"] } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } -p384 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic", "pkcs8"] } +p384 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic", "pkcs8", "ecdh"] } phf = { version = "0.14.0", default-features = false, features = ["std"] } proc-macro2 = { version = "1.0", default-features = false, features = ["proc-macro"] } pumpkin-codecs = { path = "pumpkin-codecs", default-features = false } diff --git a/pumpkin-config/src/lib.rs b/pumpkin-config/src/lib.rs index 279fe0d5d..faea0b021 100644 --- a/pumpkin-config/src/lib.rs +++ b/pumpkin-config/src/lib.rs @@ -114,6 +114,8 @@ pub struct BasicConfiguration { pub bedrock_edition: bool, /// Whether Bedrock Edition Clients are Accepted. pub bedrock_edition_address: SocketAddr, + /// Whether packet encryption is enabled for Bedrock Edition. + pub bedrock_encryption: bool, /// The seed for the world generation. pub seed: Seed, /// The maximum number of players allowed on the server. Specifying `0` disables the limit. @@ -167,6 +169,7 @@ impl Default for BasicConfiguration { java_edition_address: "0.0.0.0:25565".parse().unwrap(), bedrock_edition: true, bedrock_edition_address: "0.0.0.0:19132".parse().unwrap(), + bedrock_encryption: true, seed: Seed(random::get_seed()), max_players: 1000, view_distance: NonZeroU8::new(16).unwrap(), @@ -217,6 +220,10 @@ impl BasicConfiguration { self.encryption, "When online mode is enabled, encryption must be enabled" ); + assert!( + self.bedrock_encryption, + "When online mode is enabled, bedrock_encryption must be enabled" + ); } if self.allow_chat_reports { assert!( diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index 2c5c15ea9..a16f7e6aa 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -27,6 +27,8 @@ bytes.workspace = true # encryption aes.workspace = true cfb8.workspace = true +ctr.workspace = true +sha2.workspace = true hybrid-array = "0.4" # compression diff --git a/pumpkin-protocol/src/bedrock/client/handshake.rs b/pumpkin-protocol/src/bedrock/client/handshake.rs index fa7c365d0..a6198f686 100644 --- a/pumpkin-protocol/src/bedrock/client/handshake.rs +++ b/pumpkin-protocol/src/bedrock/client/handshake.rs @@ -1,7 +1,7 @@ +use crate::serial::PacketWrite; use pumpkin_macros::packet; -use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(PacketWrite)] #[packet(0x03)] pub struct CHandshake { jwt_data: String, diff --git a/pumpkin-protocol/src/bedrock/crypto.rs b/pumpkin-protocol/src/bedrock/crypto.rs new file mode 100644 index 000000000..ab0ee0ead --- /dev/null +++ b/pumpkin-protocol/src/bedrock/crypto.rs @@ -0,0 +1,121 @@ +use aes::Aes256; +use ctr::Ctr128BE; +use ctr::cipher::{KeyIvInit, StreamCipher}; +use sha2::{Digest, Sha256}; + +type BedrockCtr = Ctr128BE; + +pub struct BedrockEncryptor { + cipher: BedrockCtr, + key: [u8; 32], + send_counter: u64, +} + +impl BedrockEncryptor { + #[must_use] + pub fn new(key: &[u8; 32]) -> Self { + let mut iv = [0u8; 16]; + iv[..12].copy_from_slice(&key[..12]); + iv[12..].copy_from_slice(&[0, 0, 0, 2]); + + Self { + cipher: BedrockCtr::new(key.into(), &iv.into()), + key: *key, + send_counter: 0, + } + } + + pub fn encrypt(&mut self, data: &mut Vec) { + // data contains the payload after 0xfe + let mut hasher = Sha256::new(); + hasher.update(self.send_counter.to_le_bytes()); + hasher.update(&data[..]); + hasher.update(self.key); + + let hash = hasher.finalize(); + data.extend_from_slice(&hash[..8]); + + self.cipher.apply_keystream(data); + + self.send_counter += 1; + } +} + +pub struct BedrockDecryptor { + cipher: BedrockCtr, + key: [u8; 32], + send_counter: u64, +} + +impl BedrockDecryptor { + #[must_use] + pub fn new(key: &[u8; 32]) -> Self { + let mut iv = [0u8; 16]; + iv[..12].copy_from_slice(&key[..12]); + iv[12..].copy_from_slice(&[0, 0, 0, 2]); + + Self { + cipher: BedrockCtr::new(key.into(), &iv.into()), + key: *key, + send_counter: 0, + } + } + + #[expect(clippy::needless_borrow)] // False positive + pub fn decrypt(&mut self, data: &mut Vec) -> Result<(), String> { + let ciphertext = data.clone(); + self.cipher.apply_keystream(data); + + if data.len() < 8 { + return Err("Encrypted packet must be at least 8 bytes long".to_string()); + } + + let (payload, checksum) = data.split_at(data.len() - 8); + + let mut hasher = Sha256::new(); + hasher.update(self.send_counter.to_le_bytes()); + hasher.update(payload); + hasher.update(self.key); + + let our_checksum = &hasher.finalize()[..8]; + + if checksum != our_checksum { + let cipher_prefix = if ciphertext.len() > 16 { + &ciphertext[..16] + } else { + &ciphertext + }; + let plain_prefix = if data.len() > 16 { &data[..16] } else { &data }; + return Err(format!( + "Invalid checksum: expected {:x?}, got {:x?}. Cipher prefix: {:x?}, Plain prefix: {:x?}, Key: {:x?}, Counter: {}", + our_checksum, checksum, cipher_prefix, plain_prefix, self.key, self.send_counter + )); + } + + data.truncate(payload.len()); + self.send_counter += 1; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gcm_compatibility() { + let key = [1u8; 32]; + let mut iv = [0u8; 16]; + iv[..12].copy_from_slice(&key[..12]); + iv[12..].copy_from_slice(&[0, 0, 0, 2]); + + let mut cipher = BedrockCtr::new((&key).into(), (&iv).into()); + let mut data = b"Hello Bedrock encryption!".to_vec(); + cipher.apply_keystream(&mut data); + let expected = &[ + 0xfa, 0x1c, 0xd6, 0xf6, 0x06, 0xd7, 0x47, 0x96, 0x8e, 0xd7, 0x60, 0xfe, 0xc5, 0x1c, + 0x2b, 0xc7, 0x7e, 0x46, 0x17, 0x74, 0x25, 0x96, 0x34, 0x0c, 0xac, + ]; + assert_eq!(data, expected); + } +} diff --git a/pumpkin-protocol/src/bedrock/mod.rs b/pumpkin-protocol/src/bedrock/mod.rs index b68cd6cb5..addd04357 100644 --- a/pumpkin-protocol/src/bedrock/mod.rs +++ b/pumpkin-protocol/src/bedrock/mod.rs @@ -1,5 +1,6 @@ pub mod ack; pub mod client; +pub mod crypto; pub mod frame_set; pub mod network_item; pub mod packet_decoder; diff --git a/pumpkin-protocol/src/bedrock/packet_decoder.rs b/pumpkin-protocol/src/bedrock/packet_decoder.rs index 0cf0f4b51..ed3d4fa34 100644 --- a/pumpkin-protocol/src/bedrock/packet_decoder.rs +++ b/pumpkin-protocol/src/bedrock/packet_decoder.rs @@ -47,11 +47,14 @@ impl AsyncRead for DecryptionReader { } } +use crate::bedrock::crypto::BedrockDecryptor; + /// Decoder: Client -> Server /// Supports `ZLib` decoding/decompression -/// Supports Aes128 Encryption +/// Supports Aes256 Encryption pub struct UDPNetworkDecoder { compression: Option, + decryptor: Option, } impl Default for UDPNetworkDecoder { @@ -69,18 +72,21 @@ pub struct EncryptionAlreadyEnabledError; impl UDPNetworkDecoder { #[must_use] pub const fn new() -> Self { - Self { compression: None } + Self { + compression: None, + decryptor: None, + } } pub const fn set_compression(&mut self, threshold: CompressionThreshold) { self.compression = Some(threshold); } - /// NOTE: Encryption can only be set; a minecraft stream cannot go back to being unencrypted - pub const fn set_encryption( - &mut self, - _key: &[u8; 16], - ) -> Result<(), EncryptionAlreadyEnabledError> { + pub fn set_encryption(&mut self, key: &[u8; 32]) -> Result<(), EncryptionAlreadyEnabledError> { + if self.decryptor.is_some() { + return Err(EncryptionAlreadyEnabledError); + } + self.decryptor = Some(BedrockDecryptor::new(key)); Ok(()) } @@ -101,25 +107,34 @@ impl UDPNetworkDecoder { ))); } - // If compression is NOT enabled yet, the payload starts at index 1. + let mut data_to_decrypt = full_packet[1..].to_vec(); + if let Some(decryptor) = &mut self.decryptor { + decryptor + .decrypt(&mut data_to_decrypt) + .map_err(PacketDecodeError::Message)?; + } + + let full_packet_payload = data_to_decrypt; + + // If compression is NOT enabled yet, the payload starts at index 0 of full_packet_payload if self.compression.is_none() { - let payload = &full_packet[1..]; + let payload = &full_packet_payload[..]; if payload.len() > MAX_PACKET_DATA_SIZE { return Err(PacketDecodeError::TooLong); } return Ok(payload.to_vec()); } - // If compression IS enabled, Bedrock expects a compression method byte at index 1. - let compression_method = *full_packet.get(1).ok_or_else(|| { + // If compression IS enabled, Bedrock expects a compression method byte at index 0 of full_packet_payload. + let compression_method = full_packet_payload.first().ok_or_else(|| { PacketDecodeError::MalformedLength("Missing Bedrock compression method".into()) })?; - let data_start = 2; + let data_start = 1; match compression_method { 0x00 => { use tokio::io::AsyncReadExt; - let compressed_payload = &full_packet[data_start..]; + let compressed_payload = &full_packet_payload[data_start..]; let mut decoder = DeflateDecoder::new(BufReader::new(compressed_payload)) .take(MAX_PACKET_DATA_SIZE as u64 + 1); let mut decompressed = Vec::new(); @@ -134,7 +149,7 @@ impl UDPNetworkDecoder { } 0xff => { // None (Compression enabled but this specific packet is raw) - let payload = &full_packet[data_start..]; + let payload = &full_packet_payload[data_start..]; if payload.len() > MAX_PACKET_DATA_SIZE { return Err(PacketDecodeError::TooLong); } @@ -211,7 +226,7 @@ mod tests { const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1; let payload = vec![0x2a; PAYLOAD_LEN]; let mut wire_buf = Vec::new(); - let network_encoder = UDPNetworkEncoder::new(); + let mut network_encoder = UDPNetworkEncoder::new(); network_encoder .write_game_packet( 0x01, diff --git a/pumpkin-protocol/src/bedrock/packet_encoder.rs b/pumpkin-protocol/src/bedrock/packet_encoder.rs index d35312037..32eabe53c 100644 --- a/pumpkin-protocol/src/bedrock/packet_encoder.rs +++ b/pumpkin-protocol/src/bedrock/packet_encoder.rs @@ -75,12 +75,15 @@ impl AsyncWrite for EncryptionWriter { } } +use crate::bedrock::crypto::BedrockEncryptor; + /// Encoder: Server -> Client /// Supports `ZLib` endecoding/compression -/// Supports Aes128 Encryption +/// Supports Aes256 Encryption pub struct UDPNetworkEncoder { // compression and compression threshold compression: Option<(CompressionThreshold, CompressionLevel)>, + encryptor: Option, } impl Default for UDPNetworkEncoder { @@ -92,7 +95,10 @@ impl Default for UDPNetworkEncoder { impl UDPNetworkEncoder { #[must_use] pub const fn new() -> Self { - Self { compression: None } + Self { + compression: None, + encryptor: None, + } } pub const fn set_compression( @@ -102,16 +108,19 @@ impl UDPNetworkEncoder { self.compression = Some(compression_info); } - /// NOTE: Encryption can only be set; a minecraft stream cannot go back to being unencrypted - pub const fn set_encryption( + pub fn set_encryption( &mut self, - _key: &[u8; 16], + key: &[u8; 32], ) -> Result<(), crate::bedrock::packet_decoder::EncryptionAlreadyEnabledError> { + if self.encryptor.is_some() { + return Err(crate::bedrock::packet_decoder::EncryptionAlreadyEnabledError); + } + self.encryptor = Some(BedrockEncryptor::new(key)); Ok(()) } pub fn write_game_packet( - &self, + &mut self, packet_id: u16, sub_client_sender: SubClient, sub_client_target: SubClient, @@ -142,21 +151,27 @@ impl UDPNetworkEncoder { .write_u8(0xfe) .map_err(|e| Error::other(e.to_string()))?; // Bedrock Game Packet Header + let mut data_to_encrypt = Vec::new(); + if let Some((_threshold, level)) = self.compression { // Write Compression Method (0x00 for Zlib) - writer - .write_u8(0x00) - .map_err(|e| Error::other(e.to_string()))?; + data_to_encrypt.push(0x00); let mut encoder = DeflateEncoder::new(Vec::new(), Compression::new(level)); encoder.write_all(&inner_buffer)?; let compressed_data = encoder.finish()?; - writer.write_all(&compressed_data)?; + data_to_encrypt.extend_from_slice(&compressed_data); } else { - writer.write_all(&inner_buffer)?; + data_to_encrypt.extend_from_slice(&inner_buffer); } + if let Some(encryptor) = &mut self.encryptor { + encryptor.encrypt(&mut data_to_encrypt); + } + + writer.write_all(&data_to_encrypt)?; + Ok(()) } diff --git a/pumpkin-protocol/src/bedrock/server/client_to_server_handshake.rs b/pumpkin-protocol/src/bedrock/server/client_to_server_handshake.rs new file mode 100644 index 000000000..d074b63fa --- /dev/null +++ b/pumpkin-protocol/src/bedrock/server/client_to_server_handshake.rs @@ -0,0 +1,7 @@ +use pumpkin_macros::packet; + +use crate::serial::PacketRead; + +#[derive(PacketRead)] +#[packet(0x04)] +pub struct SClientToServerHandshake; diff --git a/pumpkin-protocol/src/bedrock/server/mod.rs b/pumpkin-protocol/src/bedrock/server/mod.rs index 706584a8c..be10b6f8d 100644 --- a/pumpkin-protocol/src/bedrock/server/mod.rs +++ b/pumpkin-protocol/src/bedrock/server/mod.rs @@ -2,6 +2,7 @@ pub mod actor_event; pub mod animate; pub mod block_pick_request; pub mod client_cache_status; +pub mod client_to_server_handshake; pub mod command_request; pub mod container_close; pub mod emote; @@ -28,6 +29,7 @@ pub use actor_event::*; pub use animate::*; pub use block_pick_request::*; pub use client_cache_status::*; +pub use client_to_server_handshake::*; pub use command_request::*; pub use container_close::*; pub use emote::*; diff --git a/pumpkin-util/Cargo.toml b/pumpkin-util/Cargo.toml index 341ce4e25..ef9f92594 100644 --- a/pumpkin-util/Cargo.toml +++ b/pumpkin-util/Cargo.toml @@ -25,7 +25,7 @@ enum_dispatch.workspace = true uuid.workspace = true tokio = { workspace = true, features = ["sync"] } base64.workspace = true -p384 = { workspace = true, features = ["ecdsa"] } +p384 = { workspace = true, features = ["ecdsa", "ecdh"] } thiserror.workspace = true ecdsa.workspace = true rsa.workspace = true diff --git a/pumpkin-util/src/jwt/mod.rs b/pumpkin-util/src/jwt/mod.rs index 0cdcb6b14..53bd20964 100644 --- a/pumpkin-util/src/jwt/mod.rs +++ b/pumpkin-util/src/jwt/mod.rs @@ -93,7 +93,10 @@ pub fn decode_b64_standard(s: &str) -> Result, base64::DecodeError> { /// /// A `Result` containing the `p384::PublicKey` or an `AuthError`. pub fn build_public_key_from_b64(b64: &str) -> Result { - let bytes = decode_b64_standard(b64)?; + let bytes = decode_b64_standard(b64) + .or_else(|_| general_purpose::URL_SAFE.decode(b64)) + .or_else(|_| general_purpose::URL_SAFE_NO_PAD.decode(b64)) + .map_err(AuthError::Base64Decode)?; if !bytes.is_empty() && bytes[0] == 0x30 { PublicKey::from_public_key_der(&bytes).map_err(|e| AuthError::PublicKeyBuild(e.to_string())) @@ -500,3 +503,67 @@ fn xuid_to_uuid(xuid: &str) -> String { bytes[8] = (bytes[8] & 0x3f) | 0x80; uuid::Uuid::from_bytes(bytes).to_string() } + +pub fn extract_cpk_from_token(token: &str) -> Result { + let mut parts = token.split('.'); + parts.next().ok_or(AuthError::InvalidTokenFormat)?; + let payload_b64 = parts.next().ok_or(AuthError::InvalidTokenFormat)?; + + let payload_bytes = decode_b64_url_nopad(payload_b64)?; + let v: Value = serde_json::from_slice(&payload_bytes)?; + + let cpk_b64 = v + .get("cpk") + .and_then(|v| v.as_str()) + .ok_or_else(|| AuthError::PublicKeyBuild("OIDC payload missing cpk".into()))?; + + build_public_key_from_b64(cpk_b64) +} + +pub fn generate_handshake_jwt( + signing_key: &p384::ecdsa::SigningKey, + salt: &[u8], +) -> Result { + use p384::ecdsa::signature::Signer; + use p384::pkcs8::EncodePublicKey; + + let public_key = p384::PublicKey::from(signing_key.verifying_key()); + let der_bytes = public_key + .to_public_key_der() + .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?; + + let x5u = general_purpose::STANDARD.encode(der_bytes.as_bytes()); + let salt_b64 = general_purpose::STANDARD_NO_PAD.encode(salt); + + let header_json = serde_json::json!({ + "alg": "ES384", + "x5u": x5u + }); + let payload_json = serde_json::json!({ + "salt": salt_b64 + }); + + let header_b64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header_json)?); + let payload_b64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload_json)?); + + let signing_input = format!("{header_b64}.{payload_b64}"); + + let signature: p384::ecdsa::Signature = signing_key.sign(signing_input.as_bytes()); + let signature_bytes = signature.to_bytes(); + let signature_b64 = general_purpose::URL_SAFE_NO_PAD.encode(signature_bytes); + + Ok(format!("{signing_input}.{signature_b64}")) +} + +#[must_use] +pub fn compute_shared_secret( + signing_key: &p384::ecdsa::SigningKey, + client_public_key: &p384::PublicKey, +) -> [u8; 48] { + let secret = p384::SecretKey::from(signing_key); + let shared_secret = + p384::ecdh::diffie_hellman(secret.to_nonzero_scalar(), client_public_key.as_affine()); + let mut secret_bytes = [0u8; 48]; + secret_bytes.copy_from_slice(&shared_secret.raw_secret_bytes()[..]); + secret_bytes +} diff --git a/pumpkin-util/src/lib.rs b/pumpkin-util/src/lib.rs index 5eb5d3271..f544e8aeb 100644 --- a/pumpkin-util/src/lib.rs +++ b/pumpkin-util/src/lib.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use std::ops::{Index, IndexMut}; +pub use p384; pub use serde_json; pub use difficulty::Difficulty; diff --git a/pumpkin/src/net/bedrock/login.rs b/pumpkin/src/net/bedrock/login.rs index 8a64c1352..2e953543b 100644 --- a/pumpkin/src/net/bedrock/login.rs +++ b/pumpkin/src/net/bedrock/login.rs @@ -5,6 +5,7 @@ use crate::{ server::Server, }; use arc_swap::ArcSwap; +use pumpkin_protocol::bedrock::client::handshake::CHandshake; use pumpkin_protocol::bedrock::{ client::{ network_settings::CNetworkSettings, play_status::CPlayStatus, @@ -21,8 +22,10 @@ use pumpkin_protocol::bedrock::{ use pumpkin_util::jwt::AuthError; use pumpkin_util::version::BedrockMinecraftVersion; use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION}; +use rand::RngExt; use serde::{Deserialize, de::Error}; use serde_repr::Deserialize_repr; +use sha2::{Digest, Sha256}; use std::sync::Arc; use thiserror::Error; use tracing::debug; @@ -123,15 +126,16 @@ impl BedrockClient { self: &Arc, packet: SLogin, server: &Server, - ) -> Result { + ) -> Result, LoginError> { self.try_handle_login(packet, server).await } + #[expect(clippy::too_many_lines)] pub async fn try_handle_login( self: &Arc, packet: SLogin, server: &Server, - ) -> Result { + ) -> Result, LoginError> { let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?; let player_data = if server.basic_config.online_mode { match auth_payload.authentication_type { @@ -178,6 +182,80 @@ impl BedrockClient { profile_actions: None, }; + if server.basic_config.bedrock_encryption { + let client_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token) + .map_err(LoginError::ChainValidationFailed)?; + + let (server_private_key, salt) = { + let server_key_arc = server + .bedrock_private_key + .get_or_init(|| async { + let mut rng = rand::rng(); + loop { + let mut private_key_bytes = [0u8; 48]; + for b in &mut private_key_bytes { + *b = rng.random(); + } + if let Ok(key) = pumpkin_util::p384::ecdsa::SigningKey::from_slice( + &private_key_bytes, + ) { + break Arc::new(key); + } + } + }) + .await + .clone(); + + let mut salt = [0u8; 16]; + for b in &mut salt { + *b = rand::rng().random(); + } + + (server_key_arc, salt) + }; + + let handshake_jwt = + pumpkin_util::jwt::generate_handshake_jwt(&*server_private_key, &salt) + .map_err(LoginError::ChainValidationFailed)?; + let handshake_packet = CHandshake::new(handshake_jwt); + self.send_game_packet(&handshake_packet).await; + + let shared_secret = + pumpkin_util::jwt::compute_shared_secret(&*server_private_key, &client_public_key); + let mut hasher = Sha256::new(); + hasher.update(salt); + hasher.update(shared_secret); + let key_bytes: [u8; 32] = hasher.finalize().into(); + + self.network_reader + .lock() + .await + .set_encryption(&key_bytes) + .map_err(|_| { + LoginError::ChainValidationFailed(AuthError::PublicKeyBuild( + "encryption enable error".into(), + )) + })?; + self.network_writer + .write() + .await + .set_encryption(&key_bytes) + .map_err(|_| { + LoginError::ChainValidationFailed(AuthError::PublicKeyBuild( + "encryption enable error".into(), + )) + })?; + + let new_config = PlayerConfig { + locale: client_data.language_code.clone(), + ..Default::default() + }; + self.client_data + .store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data)))); + *self.pending_profile.lock().await = Some((profile, new_config)); + return Ok(None); + } + let mut frame_set = FrameSet::default(); self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set) @@ -224,7 +302,7 @@ impl BedrockClient { self.client_data .store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data)))); - Ok(PacketHandlerResult::ReadyToPlay(profile, new_config)) + Ok(Some(PacketHandlerResult::ReadyToPlay(profile, new_config))) } pub async fn handle_resource_pack_response( diff --git a/pumpkin/src/net/bedrock/mod.rs b/pumpkin/src/net/bedrock/mod.rs index 4e64ea47d..fa47631b7 100644 --- a/pumpkin/src/net/bedrock/mod.rs +++ b/pumpkin/src/net/bedrock/mod.rs @@ -22,8 +22,11 @@ use pumpkin_protocol::{ SubClient, UDP_HEADER_SIZE, ack::Acknowledge, client::{ - disconnect_player::CDisconnectPlayer, level_chunk::CLevelChunk, + disconnect_player::CDisconnectPlayer, + level_chunk::CLevelChunk, + play_status::CPlayStatus, raknet::connection::CConnectionRequestAccepted, + resource_packs_info::{CResourcePacksInfo, ResourcePackEntry}, }, frame_set::{Frame, FrameSet}, packet_decoder::UDPNetworkDecoder, @@ -32,6 +35,7 @@ use pumpkin_protocol::{ animate::SAnimate, block_pick_request::SBlockPickRequest, client_cache_status::SClientCacheStatus, + client_to_server_handshake::SClientToServerHandshake, command_request::SCommandRequest, container_close::SContainerClose, emote::SEmote, @@ -78,7 +82,7 @@ pub mod open_connection; pub mod unconnected; use crate::{ entity::player::Player, - net::{DisconnectReason, PacketHandlerResult}, + net::{DisconnectReason, GameProfile, PacketHandlerResult, PlayerConfig}, plugin::api::events::world::chunk_send::ChunkSend, server::Server, }; @@ -157,6 +161,7 @@ pub struct BedrockClient { ordered_queues: Mutex>>, incoming_game_packet_send: Sender, incoming_game_packet_recv: Mutex>>, + pending_profile: Mutex>, } impl BedrockClient { @@ -205,6 +210,7 @@ impl BedrockClient { //input_sequence_number: AtomicU32::new(0), incoming_game_packet_send: incoming_send, incoming_game_packet_recv: Mutex::new(Some(incoming_recv)), + pending_profile: Mutex::new(None), } } @@ -277,7 +283,7 @@ impl BedrockClient { } if !resend.is_empty() { - let encoder = client.network_writer.read().await; + let encoder = client.network_writer.write().await; for (seq, id, data) in resend { debug!("Resending reliable sequence {} (ID: {})", seq, id); if let Err(err) = encoder.write_packet(&data, client.address, &client.socket).await { @@ -382,7 +388,7 @@ impl BedrockClient { let mut packets_to_enqueue = Vec::with_capacity(encoded_payloads.len()); { - let encoder = self.network_writer.read().await; + let mut encoder = self.network_writer.write().await; for payload in encoded_payloads { let mut packet_buf = Vec::new(); match encoder.write_game_packet( @@ -440,7 +446,7 @@ impl BedrockClient { } { - let Ok(network_writer) = self.network_writer.try_read() else { + let Ok(mut network_writer) = self.network_writer.try_write() else { debug!("Failed to lock network writer for try_enqueue_packet"); return; }; @@ -517,7 +523,7 @@ impl BedrockClient { let mut packet_payload = Vec::new(); packet.write_packet(&mut packet_payload)?; - let encoder = self.network_writer.read().await; + let mut encoder = self.network_writer.write().await; encoder.write_game_packet( P::PACKET_ID as u16, SubClient::Main, @@ -996,13 +1002,67 @@ impl BedrockClient { } }; match self.handle_login(packet, server).await { - Ok(result) => return result, + Ok(Some(result)) => return result, + Ok(None) => {} // encryption enabled, wait for handshake Err(err) => { self.kick(DisconnectReason::Unknown, err.to_string()).await; return PacketHandlerResult::Stop; } } } + SClientToServerHandshake::PACKET_ID => { + let _packet = match SClientToServerHandshake::read(payload) { + Ok(p) => p, + Err(err) => { + error!("Failed to read SClientToServerHandshake: {err}"); + continue; + } + }; + let pending = self.pending_profile.lock().await.take(); + if let Some((profile, new_config)) = pending { + let mut frame_set = FrameSet::default(); + self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set) + .await; + let br_config = &server.advanced_config.resource_pack.bedrock; + + let mut entries = Vec::new(); + if br_config.enabled { + for pack in &br_config.packs { + entries.push(ResourcePackEntry { + uuid: pack.uuid, + version: pack.version.clone(), + size: pack.size, + download_url: pack.download_url.clone(), + content_key: pack.content_key.clone(), + sub_pack_name: pack.sub_pack_name.clone(), + content_id: pack.content_id.clone(), + has_scripts: pack.has_scripts, + addon_pack: pack.addon_pack, + rtx_enabled: pack.rtx_enabled, + }); + } + } + + let packs_info = CResourcePacksInfo { + resource_pack_required: br_config.force, + has_addon_packs: false, + has_scripts: false, + is_vibrant_visuals_force_disabled: false, + world_template_id: uuid::Uuid::nil(), + world_template_version: String::new(), + resource_packs: entries, + }; + self.write_game_packet_to_set(&packs_info, &mut frame_set) + .await; + + self.send_frame_set(frame_set, 0x84).await; + return PacketHandlerResult::ReadyToPlay(profile, new_config); + } + error!("Received ClientToServerHandshake but no pending profile was found."); + self.kick(DisconnectReason::BadPacket, "Handshake error".into()) + .await; + return PacketHandlerResult::Stop; + } _ => { debug!( "Received unexpected game packet {} during login sequence", diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 010db0c8d..4f89810f9 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -87,6 +87,8 @@ pub struct Server { key_store: OnceCell>, /// Bedrock OIDC provider keys, fetched on startup for 1.26.10+ token validation. pub bedrock_oidc_keys: OnceCell<(String, pumpkin_util::jwt::Jwks)>, + /// Cached Bedrock server private key (process-lifetime). Generated on first Bedrock login and reused. + pub bedrock_private_key: OnceCell>, /// Manages server status information. listing: Mutex, /// Saves server branding information. @@ -259,6 +261,7 @@ impl Server { item_registry: super::item::items::default_registry(), key_store: OnceCell::new(), bedrock_oidc_keys: OnceCell::new(), + bedrock_private_key: OnceCell::new(), listing, branding: CachedBranding::new(), bossbars: Mutex::new(CustomBossbars::new()),