Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ ureq = "3.0.11"
thiserror = "2.0.12"

# Cryptography
rsa = "0.9.8"
sha1 = "0.10"
aes = "0.8"
cfb8 = "0.8"
rand = "0.9.1"
fnv = "1.0.7"
wyhash = "0.6.0"
Expand All @@ -152,6 +156,7 @@ byteorder = "1.5.0"
dashmap = "7.0.0-rc2"
uuid = { version = "1.17.0", features = ["v4", "v3", "serde"] }
indexmap = { version = "2.9.0", features = ["serde"] }
once_cell = "1.21.3"

# Macros
lazy_static = "1.5.0"
Expand Down
1 change: 1 addition & 0 deletions src/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ bevy_ecs = { workspace = true }

ferrumc-net = { workspace = true }
ferrumc-net-codec = { workspace = true }
ferrumc-net-encryption = { workspace = true }
ferrumc-plugins = { workspace = true }
ferrumc-storage = { workspace = true }
ferrumc-utils = { workspace = true }
Expand Down
3 changes: 3 additions & 0 deletions src/lib/net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ typename = { workspace = true }
bitcode = { workspace = true }
indexmap = { workspace = true }
lazy_static = { workspace = true }
aes = { workspace = true }
rsa = { workspace = true }
cfb8 = { workspace = true }
11 changes: 11 additions & 0 deletions src/lib/net/crates/encryption/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,15 @@ version = "0.1.0"
edition = "2021"

[dependencies]
ferrumc-core = { workspace = true }

thiserror = { workspace = true }
rsa = { workspace = true }
sha1 = { workspace = true }
aes = { workspace = true }
cfb8 = { workspace = true }
rand = { workspace = true }
base64 = { workspace = true }
reqwest = { workspace = true }
bevy_ecs = { workspace = true }
once_cell = { workspace = true }
42 changes: 42 additions & 0 deletions src/lib/net/crates/encryption/src/digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use sha1::{Digest, Sha1};

pub fn get_player_digest(name: &str) -> String {
let mut hash: [u8; 20] = Sha1::new().chain_update(name).finalize().into();

let negative = (hash[0] & 0x80) != 0;

if negative {
let mut carry = true;
for byte in hash.iter_mut().rev() {
*byte = !*byte;
if carry {
*byte = byte.wrapping_add(1);
carry = *byte == 0;
}
}
}

// Encode to hex
let mut hex = String::with_capacity(41);
if negative {
hex.push('-');
}
let mut started = false;
for byte in hash.iter() {
for nibble in [byte >> 4, byte & 0x0F] {
if !started {
if nibble == 0 {
continue;
}
started = true;
}
hex.push(std::char::from_digit(nibble as u32, 16).unwrap());
}
}

if hex == "-" {
"0".to_string()
} else {
hex
}
}
69 changes: 60 additions & 9 deletions src/lib/net/crates/encryption/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,67 @@
use std::sync::Arc;

use aes::{cipher::KeyInit, Aes128Dec, Aes128Enc};
use once_cell::sync::Lazy;
use rsa::{pkcs8::EncodePublicKey, rand_core::OsRng, RsaPrivateKey, RsaPublicKey};

pub mod digest;
pub mod errors;

pub fn add(left: u64, right: u64) -> u64 {
left + right
#[cfg(test)]
mod tests;

pub static ENCRYPTION_KEYS: Lazy<EncryptionKeys> = Lazy::new(|| EncryptionKeys::new());

pub struct EncryptionKeys {
pub private_key: Arc<RsaPrivateKey>,
pub public_key: Arc<RsaPublicKey>,
}

#[cfg(test)]
mod tests {
use super::*;
impl EncryptionKeys {
pub fn new() -> Self {
let mut rng = OsRng;
let private_key =
RsaPrivateKey::new(&mut rng, 1024).expect("Failed to generate PEM key for encryption");
let public_key = RsaPublicKey::from(private_key.clone());

Self {
private_key: Arc::new(private_key),
public_key: Arc::new(public_key),
}
}

pub fn get_public_der(&self) -> Vec<u8> {
self.public_key
.to_public_key_der()
.unwrap()
.as_bytes()
.to_vec()
}
}

#[derive(Clone)]
pub struct ConnectionEncryption {
pub shared_secret: Vec<u8>,
pub decrypt_cipher: Aes128Dec,
pub encrypt_cipher: Aes128Enc,
}

impl ConnectionEncryption {
pub fn new(shared_secret: Vec<u8>) -> Self {
let decrypt_cipher = Aes128Dec::new_from_slice(&shared_secret).unwrap();
let encrypt_cipher = Aes128Enc::new_from_slice(&shared_secret).unwrap();
Self {
shared_secret,
decrypt_cipher,
encrypt_cipher,
}
}

pub fn encrypt(&mut self, data: &mut [u8]) {
self.encrypt_cipher.encrypt(data);
}

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
pub fn decrypt(&mut self, data: &mut [u8]) {
self.decrypt_cipher.decrypt(data);
}
}
102 changes: 102 additions & 0 deletions src/lib/net/crates/encryption/src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use rsa::{
pkcs1::EncodeRsaPublicKey, pkcs8::DecodePublicKey, rand_core::OsRng, Pkcs1v15Encrypt,
RsaPrivateKey, RsaPublicKey,
};

use crate::{digest::get_player_digest, EncryptionKeys, ENCRYPTION_KEYS};

#[test]
fn test_pem_generation() {
let keys = EncryptionKeys::new();
let public_pem = keys
.public_key
.to_pkcs1_pem(Default::default())
.expect("failed...");

println!("Public Pem Encryption Key: {public_pem}");
println!(
"Public Pem Encryption Key (Bytes): {:?}",
public_pem.as_bytes()
);
}

#[test]
fn test_minecraft_hashes() {
assert_eq!(
get_player_digest("Notch"),
"4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
);
assert_eq!(
get_player_digest("jeb_"),
"-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
);
assert_eq!(
get_player_digest("simon"),
"88e16a1019277b15d58faf0541e11910eb756f6"
);
}

#[test]
fn test_decrypt() {
let mut rng = OsRng;
let priv_key = RsaPrivateKey::new(&mut rng, 1024).expect("failed to generate key");
let pub_key = RsaPublicKey::from(&priv_key);

let message = b"test secret";

// Encrypt using the public key
let enc = pub_key
.encrypt(&mut rng, Pkcs1v15Encrypt, message)
.expect("encryption failed");

// Decrypt using the private key
let dec = priv_key
.decrypt(Pkcs1v15Encrypt, &enc)
.expect("decryption failed");

assert_eq!(dec, message);
}

#[test]
fn test_encrypt_decrypt() {
let mut rng = OsRng;

let priv_key = RsaPrivateKey::new(&mut rng, 1024).unwrap();
let pub_key = RsaPublicKey::from(&priv_key);

let message = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // under 117 bytes
let encrypted = pub_key
.encrypt(&mut rng, Pkcs1v15Encrypt, &message)
.unwrap();

let decrypted = priv_key.decrypt(Pkcs1v15Encrypt, &encrypted).unwrap();

assert_eq!(message, decrypted);
}

#[test]
fn test_encrypt_decrypt_shared_secret() {
use rsa::pkcs8::EncodePublicKey;
use rsa::{rand_core::OsRng, Pkcs1v15Encrypt, RsaPrivateKey};

let mut rng = OsRng;

let private_key = RsaPrivateKey::new(&mut rng, 1024).unwrap();
let public_key = private_key.to_public_key();

// Get DER to simulate Minecraft client loading the key
let der = public_key.to_public_key_der().unwrap().as_bytes().to_vec();

// Simulate Minecraft client encrypting with DER key
let client_public_key = RsaPublicKey::from_public_key_der(&der).unwrap();

let secret = b"1234567890abcdef"; // 16 bytes
let encrypted = client_public_key
.encrypt(&mut rng, Pkcs1v15Encrypt, secret)
.unwrap();

// Simulate server decrypting
let decrypted = private_key.decrypt(Pkcs1v15Encrypt, &encrypted).unwrap();

assert_eq!(&decrypted, secret);
}
58 changes: 56 additions & 2 deletions src/lib/net/src/conn_init/login.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
use std::time::Instant;

use crate::conn_init::NetDecodeOpts;
use crate::conn_init::VarInt;
use crate::conn_init::{send_packet, trim_packet_head};
use crate::errors::NetError;
use crate::packets::outgoing::encryption_request::EncryptionRequestPacket;
use crate::packets::outgoing::registry_data::REGISTRY_PACKETS;
use ferrumc_config::statics::get_global_config;
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_net_codec::decode::NetDecode;
use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec;
use ferrumc_net_encryption::ConnectionEncryption;
use ferrumc_net_encryption::ENCRYPTION_KEYS;
use ferrumc_state::GlobalState;
use rand::RngCore;
use rsa::Pkcs1v15Encrypt;
use tokio::io::AsyncReadExt;
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tracing::debug;
use tracing::{error, trace};

pub(super) async fn login(
mut conn_read: &mut OwnedReadHalf,
conn_write: &mut OwnedWriteHalf,
state: GlobalState,
) -> Result<(bool, Option<PlayerIdentity>), NetError> {
) -> Result<(bool, Option<(ConnectionEncryption, PlayerIdentity)>), NetError> {
// =============================================================================================
trim_packet_head(conn_read, 0x00).await?;

Expand All @@ -26,6 +34,52 @@ pub(super) async fn login(
)
.await?;

debug!("Encrypting...");

let instant = Instant::now();
let should_authenticate = get_global_config().online_mode;
let mut verify_token = [0u8; 16];
rand::rng().fill_bytes(&mut verify_token);

let encryption_request = EncryptionRequestPacket {
server_id: String::new(),
public_key: LengthPrefixedVec::new(ENCRYPTION_KEYS.get_public_der()),
verify_token: LengthPrefixedVec::new(verify_token.to_vec()),
should_authenticate,
};

send_packet(conn_write, &encryption_request).await?;

trim_packet_head(conn_read, 0x01).await?;
let encryption_response =
crate::packets::incoming::encryption_response::EncryptionResponsePacket::decode_async(
&mut conn_read,
&NetDecodeOpts::None,
)
.await?;

// Decrypt, this has to be here since the response packet is decrypted differently with other packets
// This is because the response doesn't use the shared_key for decryption, since this is the packet that sends it.
let decrypted_secret = ENCRYPTION_KEYS
.private_key
.decrypt(Pkcs1v15Encrypt, &encryption_response.shared_secret.data)
.map_err(|_| NetError::Auth("Failed to decrypt shared_key".to_string()))?;

let decrypted_token = ENCRYPTION_KEYS
.private_key
.decrypt(Pkcs1v15Encrypt, &encryption_response.verify_token.data)
.map_err(|_| NetError::Auth("Failed to decrypt verify_token".to_string()))?;

if verify_token != decrypted_token.as_slice() {
return Err(NetError::Auth(
"Verify Token Mismatch, client may not have the correct encryption key...".to_string(),
));
}

// This results in encryption being successful
let player_encryption = ConnectionEncryption::new(decrypted_secret);
debug!("Encryption Completed... (Took: {:?})", instant.elapsed());

// =============================================================================================

let login_success = crate::packets::outgoing::login_success::LoginSuccessPacket {
Expand Down Expand Up @@ -207,5 +261,5 @@ pub(super) async fn login(
}
}

Ok((false, Some(player_identity)))
Ok((false, Some((player_encryption, player_identity))))
}
Loading
Loading