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"
|
||||
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",
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
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 client;
|
||||
pub mod crypto;
|
||||
pub mod frame_set;
|
||||
pub mod network_item;
|
||||
pub mod packet_decoder;
|
||||
|
||||
@ -47,11 +47,14 @@ impl<R: AsyncRead + Unpin> AsyncRead for DecryptionReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::bedrock::crypto::BedrockDecryptor;
|
||||
|
||||
/// Decoder: Client -> Server
|
||||
/// Supports `ZLib` decoding/decompression
|
||||
/// Supports Aes128 Encryption
|
||||
/// Supports Aes256 Encryption
|
||||
pub struct UDPNetworkDecoder {
|
||||
compression: Option<CompressionThreshold>,
|
||||
decryptor: Option<BedrockDecryptor>,
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@ -75,12 +75,15 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for EncryptionWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
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<BedrockEncryptor>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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 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::*;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
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 {
|
||||
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<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 std::ops::{Index, IndexMut};
|
||||
|
||||
pub use p384;
|
||||
pub use serde_json;
|
||||
|
||||
pub use difficulty::Difficulty;
|
||||
|
||||
@ -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<Self>,
|
||||
packet: SLogin,
|
||||
server: &Server,
|
||||
) -> Result<PacketHandlerResult, LoginError> {
|
||||
) -> Result<Option<PacketHandlerResult>, LoginError> {
|
||||
self.try_handle_login(packet, server).await
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub async fn try_handle_login(
|
||||
self: &Arc<Self>,
|
||||
packet: SLogin,
|
||||
server: &Server,
|
||||
) -> Result<PacketHandlerResult, LoginError> {
|
||||
) -> Result<Option<PacketHandlerResult>, 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(
|
||||
|
||||
@ -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<HashMap<u8, BTreeMap<u32, Frame>>>,
|
||||
incoming_game_packet_send: Sender<RawPacket>,
|
||||
incoming_game_packet_recv: Mutex<Option<Receiver<RawPacket>>>,
|
||||
pending_profile: Mutex<Option<(GameProfile, PlayerConfig)>>,
|
||||
}
|
||||
|
||||
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",
|
||||
|
||||
@ -87,6 +87,8 @@ pub struct Server {
|
||||
key_store: OnceCell<Arc<KeyStore>>,
|
||||
/// 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<Arc<pumpkin_util::p384::ecdsa::SigningKey>>,
|
||||
/// Manages server status information.
|
||||
listing: Mutex<CachedStatus>,
|
||||
/// 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()),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user