feat(bedrock): Add Protocol encryption
This commit is contained in:
parent
52838d945b
commit
c60fa90ed8
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -431,6 +431,7 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"block-buffer 0.12.1",
|
||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
@ -964,6 +965,15 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"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]]
|
[[package]]
|
||||||
name = "ctutils"
|
name = "ctutils"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@ -1111,6 +1121,7 @@ dependencies = [
|
|||||||
"ff",
|
"ff",
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"group",
|
"group",
|
||||||
|
"hkdf",
|
||||||
"pkcs8 0.10.2",
|
"pkcs8 0.10.2",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"sec1",
|
"sec1",
|
||||||
@ -1513,6 +1524,15 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
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]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@ -2810,6 +2830,7 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cfb8",
|
"cfb8",
|
||||||
|
"ctr",
|
||||||
"flate2",
|
"flate2",
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
"pumpkin-data",
|
"pumpkin-data",
|
||||||
@ -2818,6 +2839,7 @@ dependencies = [
|
|||||||
"pumpkin-util",
|
"pumpkin-util",
|
||||||
"pumpkin-world",
|
"pumpkin-world",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sha2 0.11.0",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|||||||
@ -123,6 +123,7 @@ base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
|||||||
bitflags = { version = "2.13.0", default-features = false, features = ["std"] }
|
bitflags = { version = "2.13.0", default-features = false, features = ["std"] }
|
||||||
cesu8 = { version = "1.1", default-features = false }
|
cesu8 = { version = "1.1", default-features = false }
|
||||||
cfb8 = { version = "0.9", 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 }
|
colored = { version = "3.1", default-features = false }
|
||||||
console-subscriber = { version = "0.5.0", default-features = false }
|
console-subscriber = { version = "0.5.0", default-features = false }
|
||||||
crc-fast = { version = "1.10.0", default-features = false, features = ["std"] }
|
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-bigint = { version = "0.5", default-features = false, features = ["std"] }
|
||||||
num-derive = { version = "0.4", default-features = false }
|
num-derive = { version = "0.4", default-features = false }
|
||||||
num-traits = { version = "0.2", default-features = false, features = ["std"] }
|
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"] }
|
phf = { version = "0.14.0", default-features = false, features = ["std"] }
|
||||||
proc-macro2 = { version = "1.0", default-features = false, features = ["proc-macro"] }
|
proc-macro2 = { version = "1.0", default-features = false, features = ["proc-macro"] }
|
||||||
pumpkin-codecs = { path = "pumpkin-codecs", default-features = false }
|
pumpkin-codecs = { path = "pumpkin-codecs", default-features = false }
|
||||||
|
|||||||
@ -114,6 +114,8 @@ pub struct BasicConfiguration {
|
|||||||
pub bedrock_edition: bool,
|
pub bedrock_edition: bool,
|
||||||
/// Whether Bedrock Edition Clients are Accepted.
|
/// Whether Bedrock Edition Clients are Accepted.
|
||||||
pub bedrock_edition_address: SocketAddr,
|
pub bedrock_edition_address: SocketAddr,
|
||||||
|
/// Whether packet encryption is enabled for Bedrock Edition.
|
||||||
|
pub bedrock_encryption: bool,
|
||||||
/// The seed for the world generation.
|
/// The seed for the world generation.
|
||||||
pub seed: Seed,
|
pub seed: Seed,
|
||||||
/// The maximum number of players allowed on the server. Specifying `0` disables the limit.
|
/// 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(),
|
java_edition_address: "0.0.0.0:25565".parse().unwrap(),
|
||||||
bedrock_edition: true,
|
bedrock_edition: true,
|
||||||
bedrock_edition_address: "0.0.0.0:19132".parse().unwrap(),
|
bedrock_edition_address: "0.0.0.0:19132".parse().unwrap(),
|
||||||
|
bedrock_encryption: true,
|
||||||
seed: Seed(random::get_seed()),
|
seed: Seed(random::get_seed()),
|
||||||
max_players: 1000,
|
max_players: 1000,
|
||||||
view_distance: NonZeroU8::new(16).unwrap(),
|
view_distance: NonZeroU8::new(16).unwrap(),
|
||||||
@ -217,6 +220,10 @@ impl BasicConfiguration {
|
|||||||
self.encryption,
|
self.encryption,
|
||||||
"When online mode is enabled, encryption must be enabled"
|
"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 {
|
if self.allow_chat_reports {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@ -27,6 +27,8 @@ bytes.workspace = true
|
|||||||
# encryption
|
# encryption
|
||||||
aes.workspace = true
|
aes.workspace = true
|
||||||
cfb8.workspace = true
|
cfb8.workspace = true
|
||||||
|
ctr.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
hybrid-array = "0.4"
|
hybrid-array = "0.4"
|
||||||
|
|
||||||
# compression
|
# compression
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
use crate::serial::PacketWrite;
|
||||||
use pumpkin_macros::packet;
|
use pumpkin_macros::packet;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(PacketWrite)]
|
||||||
#[packet(0x03)]
|
#[packet(0x03)]
|
||||||
pub struct CHandshake {
|
pub struct CHandshake {
|
||||||
jwt_data: String,
|
jwt_data: String,
|
||||||
|
|||||||
121
pumpkin-protocol/src/bedrock/crypto.rs
Normal file
121
pumpkin-protocol/src/bedrock/crypto.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
use aes::Aes256;
|
||||||
|
use ctr::Ctr128BE;
|
||||||
|
use ctr::cipher::{KeyIvInit, StreamCipher};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
type BedrockCtr = Ctr128BE<Aes256>;
|
||||||
|
|
||||||
|
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<u8>) {
|
||||||
|
// 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<u8>) -> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
pub mod ack;
|
pub mod ack;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod crypto;
|
||||||
pub mod frame_set;
|
pub mod frame_set;
|
||||||
pub mod network_item;
|
pub mod network_item;
|
||||||
pub mod packet_decoder;
|
pub mod packet_decoder;
|
||||||
|
|||||||
@ -47,11 +47,14 @@ impl<R: AsyncRead + Unpin> AsyncRead for DecryptionReader<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::bedrock::crypto::BedrockDecryptor;
|
||||||
|
|
||||||
/// Decoder: Client -> Server
|
/// Decoder: Client -> Server
|
||||||
/// Supports `ZLib` decoding/decompression
|
/// Supports `ZLib` decoding/decompression
|
||||||
/// Supports Aes128 Encryption
|
/// Supports Aes256 Encryption
|
||||||
pub struct UDPNetworkDecoder {
|
pub struct UDPNetworkDecoder {
|
||||||
compression: Option<CompressionThreshold>,
|
compression: Option<CompressionThreshold>,
|
||||||
|
decryptor: Option<BedrockDecryptor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UDPNetworkDecoder {
|
impl Default for UDPNetworkDecoder {
|
||||||
@ -69,18 +72,21 @@ pub struct EncryptionAlreadyEnabledError;
|
|||||||
impl UDPNetworkDecoder {
|
impl UDPNetworkDecoder {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self { compression: None }
|
Self {
|
||||||
|
compression: None,
|
||||||
|
decryptor: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn set_compression(&mut self, threshold: CompressionThreshold) {
|
pub const fn set_compression(&mut self, threshold: CompressionThreshold) {
|
||||||
self.compression = Some(threshold);
|
self.compression = Some(threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: Encryption can only be set; a minecraft stream cannot go back to being unencrypted
|
pub fn set_encryption(&mut self, key: &[u8; 32]) -> Result<(), EncryptionAlreadyEnabledError> {
|
||||||
pub const fn set_encryption(
|
if self.decryptor.is_some() {
|
||||||
&mut self,
|
return Err(EncryptionAlreadyEnabledError);
|
||||||
_key: &[u8; 16],
|
}
|
||||||
) -> Result<(), EncryptionAlreadyEnabledError> {
|
self.decryptor = Some(BedrockDecryptor::new(key));
|
||||||
Ok(())
|
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() {
|
if self.compression.is_none() {
|
||||||
let payload = &full_packet[1..];
|
let payload = &full_packet_payload[..];
|
||||||
if payload.len() > MAX_PACKET_DATA_SIZE {
|
if payload.len() > MAX_PACKET_DATA_SIZE {
|
||||||
return Err(PacketDecodeError::TooLong);
|
return Err(PacketDecodeError::TooLong);
|
||||||
}
|
}
|
||||||
return Ok(payload.to_vec());
|
return Ok(payload.to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If compression IS enabled, Bedrock expects a compression method byte at index 1.
|
// If compression IS enabled, Bedrock expects a compression method byte at index 0 of full_packet_payload.
|
||||||
let compression_method = *full_packet.get(1).ok_or_else(|| {
|
let compression_method = full_packet_payload.first().ok_or_else(|| {
|
||||||
PacketDecodeError::MalformedLength("Missing Bedrock compression method".into())
|
PacketDecodeError::MalformedLength("Missing Bedrock compression method".into())
|
||||||
})?;
|
})?;
|
||||||
let data_start = 2;
|
let data_start = 1;
|
||||||
|
|
||||||
match compression_method {
|
match compression_method {
|
||||||
0x00 => {
|
0x00 => {
|
||||||
use tokio::io::AsyncReadExt;
|
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))
|
let mut decoder = DeflateDecoder::new(BufReader::new(compressed_payload))
|
||||||
.take(MAX_PACKET_DATA_SIZE as u64 + 1);
|
.take(MAX_PACKET_DATA_SIZE as u64 + 1);
|
||||||
let mut decompressed = Vec::new();
|
let mut decompressed = Vec::new();
|
||||||
@ -134,7 +149,7 @@ impl UDPNetworkDecoder {
|
|||||||
}
|
}
|
||||||
0xff => {
|
0xff => {
|
||||||
// None (Compression enabled but this specific packet is raw)
|
// 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 {
|
if payload.len() > MAX_PACKET_DATA_SIZE {
|
||||||
return Err(PacketDecodeError::TooLong);
|
return Err(PacketDecodeError::TooLong);
|
||||||
}
|
}
|
||||||
@ -211,7 +226,7 @@ mod tests {
|
|||||||
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
|
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
|
||||||
let payload = vec![0x2a; PAYLOAD_LEN];
|
let payload = vec![0x2a; PAYLOAD_LEN];
|
||||||
let mut wire_buf = Vec::new();
|
let mut wire_buf = Vec::new();
|
||||||
let network_encoder = UDPNetworkEncoder::new();
|
let mut network_encoder = UDPNetworkEncoder::new();
|
||||||
network_encoder
|
network_encoder
|
||||||
.write_game_packet(
|
.write_game_packet(
|
||||||
0x01,
|
0x01,
|
||||||
|
|||||||
@ -75,12 +75,15 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for EncryptionWriter<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::bedrock::crypto::BedrockEncryptor;
|
||||||
|
|
||||||
/// Encoder: Server -> Client
|
/// Encoder: Server -> Client
|
||||||
/// Supports `ZLib` endecoding/compression
|
/// Supports `ZLib` endecoding/compression
|
||||||
/// Supports Aes128 Encryption
|
/// Supports Aes256 Encryption
|
||||||
pub struct UDPNetworkEncoder {
|
pub struct UDPNetworkEncoder {
|
||||||
// compression and compression threshold
|
// compression and compression threshold
|
||||||
compression: Option<(CompressionThreshold, CompressionLevel)>,
|
compression: Option<(CompressionThreshold, CompressionLevel)>,
|
||||||
|
encryptor: Option<BedrockEncryptor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UDPNetworkEncoder {
|
impl Default for UDPNetworkEncoder {
|
||||||
@ -92,7 +95,10 @@ impl Default for UDPNetworkEncoder {
|
|||||||
impl UDPNetworkEncoder {
|
impl UDPNetworkEncoder {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self { compression: None }
|
Self {
|
||||||
|
compression: None,
|
||||||
|
encryptor: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn set_compression(
|
pub const fn set_compression(
|
||||||
@ -102,16 +108,19 @@ impl UDPNetworkEncoder {
|
|||||||
self.compression = Some(compression_info);
|
self.compression = Some(compression_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: Encryption can only be set; a minecraft stream cannot go back to being unencrypted
|
pub fn set_encryption(
|
||||||
pub const fn set_encryption(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
_key: &[u8; 16],
|
key: &[u8; 32],
|
||||||
) -> Result<(), crate::bedrock::packet_decoder::EncryptionAlreadyEnabledError> {
|
) -> 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_game_packet(
|
pub fn write_game_packet(
|
||||||
&self,
|
&mut self,
|
||||||
packet_id: u16,
|
packet_id: u16,
|
||||||
sub_client_sender: SubClient,
|
sub_client_sender: SubClient,
|
||||||
sub_client_target: SubClient,
|
sub_client_target: SubClient,
|
||||||
@ -142,21 +151,27 @@ impl UDPNetworkEncoder {
|
|||||||
.write_u8(0xfe)
|
.write_u8(0xfe)
|
||||||
.map_err(|e| Error::other(e.to_string()))?; // Bedrock Game Packet Header
|
.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 {
|
if let Some((_threshold, level)) = self.compression {
|
||||||
// Write Compression Method (0x00 for Zlib)
|
// Write Compression Method (0x00 for Zlib)
|
||||||
writer
|
data_to_encrypt.push(0x00);
|
||||||
.write_u8(0x00)
|
|
||||||
.map_err(|e| Error::other(e.to_string()))?;
|
|
||||||
|
|
||||||
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::new(level));
|
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::new(level));
|
||||||
encoder.write_all(&inner_buffer)?;
|
encoder.write_all(&inner_buffer)?;
|
||||||
let compressed_data = encoder.finish()?;
|
let compressed_data = encoder.finish()?;
|
||||||
|
|
||||||
writer.write_all(&compressed_data)?;
|
data_to_encrypt.extend_from_slice(&compressed_data);
|
||||||
} else {
|
} 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
use pumpkin_macros::packet;
|
||||||
|
|
||||||
|
use crate::serial::PacketRead;
|
||||||
|
|
||||||
|
#[derive(PacketRead)]
|
||||||
|
#[packet(0x04)]
|
||||||
|
pub struct SClientToServerHandshake;
|
||||||
@ -2,6 +2,7 @@ pub mod actor_event;
|
|||||||
pub mod animate;
|
pub mod animate;
|
||||||
pub mod block_pick_request;
|
pub mod block_pick_request;
|
||||||
pub mod client_cache_status;
|
pub mod client_cache_status;
|
||||||
|
pub mod client_to_server_handshake;
|
||||||
pub mod command_request;
|
pub mod command_request;
|
||||||
pub mod container_close;
|
pub mod container_close;
|
||||||
pub mod emote;
|
pub mod emote;
|
||||||
@ -28,6 +29,7 @@ pub use actor_event::*;
|
|||||||
pub use animate::*;
|
pub use animate::*;
|
||||||
pub use block_pick_request::*;
|
pub use block_pick_request::*;
|
||||||
pub use client_cache_status::*;
|
pub use client_cache_status::*;
|
||||||
|
pub use client_to_server_handshake::*;
|
||||||
pub use command_request::*;
|
pub use command_request::*;
|
||||||
pub use container_close::*;
|
pub use container_close::*;
|
||||||
pub use emote::*;
|
pub use emote::*;
|
||||||
|
|||||||
@ -25,7 +25,7 @@ enum_dispatch.workspace = true
|
|||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
tokio = { workspace = true, features = ["sync"] }
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
p384 = { workspace = true, features = ["ecdsa"] }
|
p384 = { workspace = true, features = ["ecdsa", "ecdh"] }
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
ecdsa.workspace = true
|
ecdsa.workspace = true
|
||||||
rsa.workspace = true
|
rsa.workspace = true
|
||||||
|
|||||||
@ -93,7 +93,10 @@ pub fn decode_b64_standard(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
|||||||
///
|
///
|
||||||
/// A `Result` containing the `p384::PublicKey` or an `AuthError`.
|
/// A `Result` containing the `p384::PublicKey` or an `AuthError`.
|
||||||
pub fn build_public_key_from_b64(b64: &str) -> Result<PublicKey, AuthError> {
|
pub fn build_public_key_from_b64(b64: &str) -> Result<PublicKey, AuthError> {
|
||||||
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 {
|
if !bytes.is_empty() && bytes[0] == 0x30 {
|
||||||
PublicKey::from_public_key_der(&bytes).map_err(|e| AuthError::PublicKeyBuild(e.to_string()))
|
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;
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
uuid::Uuid::from_bytes(bytes).to_string()
|
uuid::Uuid::from_bytes(bytes).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extract_cpk_from_token(token: &str) -> Result<PublicKey, AuthError> {
|
||||||
|
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<String, AuthError> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ops::{Index, IndexMut};
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
|
pub use p384;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
|
|
||||||
pub use difficulty::Difficulty;
|
pub use difficulty::Difficulty;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use crate::{
|
|||||||
server::Server,
|
server::Server,
|
||||||
};
|
};
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
|
use pumpkin_protocol::bedrock::client::handshake::CHandshake;
|
||||||
use pumpkin_protocol::bedrock::{
|
use pumpkin_protocol::bedrock::{
|
||||||
client::{
|
client::{
|
||||||
network_settings::CNetworkSettings, play_status::CPlayStatus,
|
network_settings::CNetworkSettings, play_status::CPlayStatus,
|
||||||
@ -21,8 +22,10 @@ use pumpkin_protocol::bedrock::{
|
|||||||
use pumpkin_util::jwt::AuthError;
|
use pumpkin_util::jwt::AuthError;
|
||||||
use pumpkin_util::version::BedrockMinecraftVersion;
|
use pumpkin_util::version::BedrockMinecraftVersion;
|
||||||
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
|
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
|
||||||
|
use rand::RngExt;
|
||||||
use serde::{Deserialize, de::Error};
|
use serde::{Deserialize, de::Error};
|
||||||
use serde_repr::Deserialize_repr;
|
use serde_repr::Deserialize_repr;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@ -123,15 +126,16 @@ impl BedrockClient {
|
|||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
packet: SLogin,
|
packet: SLogin,
|
||||||
server: &Server,
|
server: &Server,
|
||||||
) -> Result<PacketHandlerResult, LoginError> {
|
) -> Result<Option<PacketHandlerResult>, LoginError> {
|
||||||
self.try_handle_login(packet, server).await
|
self.try_handle_login(packet, server).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_lines)]
|
||||||
pub async fn try_handle_login(
|
pub async fn try_handle_login(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
packet: SLogin,
|
packet: SLogin,
|
||||||
server: &Server,
|
server: &Server,
|
||||||
) -> Result<PacketHandlerResult, LoginError> {
|
) -> Result<Option<PacketHandlerResult>, LoginError> {
|
||||||
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
|
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
|
||||||
let player_data = if server.basic_config.online_mode {
|
let player_data = if server.basic_config.online_mode {
|
||||||
match auth_payload.authentication_type {
|
match auth_payload.authentication_type {
|
||||||
@ -178,6 +182,80 @@ impl BedrockClient {
|
|||||||
profile_actions: None,
|
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();
|
let mut frame_set = FrameSet::default();
|
||||||
|
|
||||||
self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set)
|
self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set)
|
||||||
@ -224,7 +302,7 @@ impl BedrockClient {
|
|||||||
self.client_data
|
self.client_data
|
||||||
.store(std::sync::Arc::new(Some(std::sync::Arc::new(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(
|
pub async fn handle_resource_pack_response(
|
||||||
|
|||||||
@ -22,8 +22,11 @@ use pumpkin_protocol::{
|
|||||||
SubClient, UDP_HEADER_SIZE,
|
SubClient, UDP_HEADER_SIZE,
|
||||||
ack::Acknowledge,
|
ack::Acknowledge,
|
||||||
client::{
|
client::{
|
||||||
disconnect_player::CDisconnectPlayer, level_chunk::CLevelChunk,
|
disconnect_player::CDisconnectPlayer,
|
||||||
|
level_chunk::CLevelChunk,
|
||||||
|
play_status::CPlayStatus,
|
||||||
raknet::connection::CConnectionRequestAccepted,
|
raknet::connection::CConnectionRequestAccepted,
|
||||||
|
resource_packs_info::{CResourcePacksInfo, ResourcePackEntry},
|
||||||
},
|
},
|
||||||
frame_set::{Frame, FrameSet},
|
frame_set::{Frame, FrameSet},
|
||||||
packet_decoder::UDPNetworkDecoder,
|
packet_decoder::UDPNetworkDecoder,
|
||||||
@ -32,6 +35,7 @@ use pumpkin_protocol::{
|
|||||||
animate::SAnimate,
|
animate::SAnimate,
|
||||||
block_pick_request::SBlockPickRequest,
|
block_pick_request::SBlockPickRequest,
|
||||||
client_cache_status::SClientCacheStatus,
|
client_cache_status::SClientCacheStatus,
|
||||||
|
client_to_server_handshake::SClientToServerHandshake,
|
||||||
command_request::SCommandRequest,
|
command_request::SCommandRequest,
|
||||||
container_close::SContainerClose,
|
container_close::SContainerClose,
|
||||||
emote::SEmote,
|
emote::SEmote,
|
||||||
@ -78,7 +82,7 @@ pub mod open_connection;
|
|||||||
pub mod unconnected;
|
pub mod unconnected;
|
||||||
use crate::{
|
use crate::{
|
||||||
entity::player::Player,
|
entity::player::Player,
|
||||||
net::{DisconnectReason, PacketHandlerResult},
|
net::{DisconnectReason, GameProfile, PacketHandlerResult, PlayerConfig},
|
||||||
plugin::api::events::world::chunk_send::ChunkSend,
|
plugin::api::events::world::chunk_send::ChunkSend,
|
||||||
server::Server,
|
server::Server,
|
||||||
};
|
};
|
||||||
@ -157,6 +161,7 @@ pub struct BedrockClient {
|
|||||||
ordered_queues: Mutex<HashMap<u8, BTreeMap<u32, Frame>>>,
|
ordered_queues: Mutex<HashMap<u8, BTreeMap<u32, Frame>>>,
|
||||||
incoming_game_packet_send: Sender<RawPacket>,
|
incoming_game_packet_send: Sender<RawPacket>,
|
||||||
incoming_game_packet_recv: Mutex<Option<Receiver<RawPacket>>>,
|
incoming_game_packet_recv: Mutex<Option<Receiver<RawPacket>>>,
|
||||||
|
pending_profile: Mutex<Option<(GameProfile, PlayerConfig)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BedrockClient {
|
impl BedrockClient {
|
||||||
@ -205,6 +210,7 @@ impl BedrockClient {
|
|||||||
//input_sequence_number: AtomicU32::new(0),
|
//input_sequence_number: AtomicU32::new(0),
|
||||||
incoming_game_packet_send: incoming_send,
|
incoming_game_packet_send: incoming_send,
|
||||||
incoming_game_packet_recv: Mutex::new(Some(incoming_recv)),
|
incoming_game_packet_recv: Mutex::new(Some(incoming_recv)),
|
||||||
|
pending_profile: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +283,7 @@ impl BedrockClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !resend.is_empty() {
|
if !resend.is_empty() {
|
||||||
let encoder = client.network_writer.read().await;
|
let encoder = client.network_writer.write().await;
|
||||||
for (seq, id, data) in resend {
|
for (seq, id, data) in resend {
|
||||||
debug!("Resending reliable sequence {} (ID: {})", seq, id);
|
debug!("Resending reliable sequence {} (ID: {})", seq, id);
|
||||||
if let Err(err) = encoder.write_packet(&data, client.address, &client.socket).await {
|
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 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 {
|
for payload in encoded_payloads {
|
||||||
let mut packet_buf = Vec::new();
|
let mut packet_buf = Vec::new();
|
||||||
match encoder.write_game_packet(
|
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");
|
debug!("Failed to lock network writer for try_enqueue_packet");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -517,7 +523,7 @@ impl BedrockClient {
|
|||||||
let mut packet_payload = Vec::new();
|
let mut packet_payload = Vec::new();
|
||||||
packet.write_packet(&mut packet_payload)?;
|
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(
|
encoder.write_game_packet(
|
||||||
P::PACKET_ID as u16,
|
P::PACKET_ID as u16,
|
||||||
SubClient::Main,
|
SubClient::Main,
|
||||||
@ -996,13 +1002,67 @@ impl BedrockClient {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
match self.handle_login(packet, server).await {
|
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) => {
|
Err(err) => {
|
||||||
self.kick(DisconnectReason::Unknown, err.to_string()).await;
|
self.kick(DisconnectReason::Unknown, err.to_string()).await;
|
||||||
return PacketHandlerResult::Stop;
|
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!(
|
debug!(
|
||||||
"Received unexpected game packet {} during login sequence",
|
"Received unexpected game packet {} during login sequence",
|
||||||
|
|||||||
@ -87,6 +87,8 @@ pub struct Server {
|
|||||||
key_store: OnceCell<Arc<KeyStore>>,
|
key_store: OnceCell<Arc<KeyStore>>,
|
||||||
/// Bedrock OIDC provider keys, fetched on startup for 1.26.10+ token validation.
|
/// Bedrock OIDC provider keys, fetched on startup for 1.26.10+ token validation.
|
||||||
pub bedrock_oidc_keys: OnceCell<(String, pumpkin_util::jwt::Jwks)>,
|
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<Arc<pumpkin_util::p384::ecdsa::SigningKey>>,
|
||||||
/// Manages server status information.
|
/// Manages server status information.
|
||||||
listing: Mutex<CachedStatus>,
|
listing: Mutex<CachedStatus>,
|
||||||
/// Saves server branding information.
|
/// Saves server branding information.
|
||||||
@ -259,6 +261,7 @@ impl Server {
|
|||||||
item_registry: super::item::items::default_registry(),
|
item_registry: super::item::items::default_registry(),
|
||||||
key_store: OnceCell::new(),
|
key_store: OnceCell::new(),
|
||||||
bedrock_oidc_keys: OnceCell::new(),
|
bedrock_oidc_keys: OnceCell::new(),
|
||||||
|
bedrock_private_key: OnceCell::new(),
|
||||||
listing,
|
listing,
|
||||||
branding: CachedBranding::new(),
|
branding: CachedBranding::new(),
|
||||||
bossbars: Mutex::new(CustomBossbars::new()),
|
bossbars: Mutex::new(CustomBossbars::new()),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user