Skip to content

Commit

Permalink
CMD login + game client encryption support
Browse files Browse the repository at this point in the history
  • Loading branch information
artemijan committed Feb 2, 2025
1 parent 5b54e9e commit 5ff5bca
Show file tree
Hide file tree
Showing 17 changed files with 273 additions and 83 deletions.
9 changes: 6 additions & 3 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ iconic Kozak weapon. It is used here metaphorically and holds no association wit

Current state: `Under development`

About: This implementation is based on L2J, e.g. I reimplement everything in rust from java + I optimize things,
About: This implementation is based on L2J, e.g. I reimplement everything in rust from java + I optimize things,
like DB denormalization for performance boost, removing unnecessary operations and so on.

Ready features:
Expand All @@ -18,13 +18,16 @@ Ready features:
- [x] Login process + auto create accounts
- [x] Game server registration
- [x] Kicking player
- [ ] CMD login
- [x] CMD login
- [ ] EBPF filtering of banned IPs
- [x] Game server
- [x] register o login server
- [x] player packet encryption support (shifting key)
- [x] player can select game server
- [x] player can create char
- [ ] player can delete char
- [x] player can delete char
- [x] player can restore char
- [x] player can select char
- [ ] player can enter the game world

## The client
Expand Down
3 changes: 1 addition & 2 deletions config/game.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ client:
timeout: 150
allowed_revisions:
- 110
- 6553697
server_id: 1
hex_id: -2ad66b3f483c22be097019f55c8abdf0
accept_alternative_id: true
Expand All @@ -18,7 +17,7 @@ server_type: Normal
max_players: 5000
rates:
vitality_exp_multiplier: 2
enable_encryption: false
enable_encryption: true
#ip_config:
# - subnet: 192.168.0.0/0
# ip: 192.168.0.16
Expand Down
1 change: 0 additions & 1 deletion config/login.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
name: Login server
blowfish_key: "_;v.]05-31!|+-%xT!^[$\0"
auto_registration: true
runtime:
worker_threads: 10
client:
Expand Down
40 changes: 27 additions & 13 deletions game/src/client_thread/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use entities::dao::char_info::CharacterInfo;
use entities::entities::{character, user};
use entities::DBPool;
use l2_core::config::gs::GSServer;
use l2_core::crypt::game::GameClientEncryption;
use l2_core::crypt::generate_blowfish_key;
use l2_core::crypt::login::Encryption;
use l2_core::dto::InboundConnection;
Expand Down Expand Up @@ -41,7 +42,7 @@ pub struct ClientHandler {
shutdown_notifier: Arc<Notify>,
timeout: u8,
ip: Ipv4Addr,
blowfish: Option<Encryption>,
blowfish: Option<Arc<Mutex<GameClientEncryption>>>,
protocol: Option<i32>,
status: ClientStatus,
account_chars: Option<Vec<CharacterInfo>>,
Expand Down Expand Up @@ -167,8 +168,12 @@ impl ClientHandler {
.ok_or(anyhow::anyhow!("Missing character at slot {slot_id}"))
}

pub fn set_encryption(&mut self, bf_key: Option<Encryption>) {
self.blowfish = bf_key;
pub fn set_encryption(&mut self, bf_key: Option<GameClientEncryption>) {
if let Some(key) = bf_key {
self.blowfish = Some(Arc::new(Mutex::new(key)));
} else {
self.blowfish = None;
}
}
pub fn generate_key() -> Vec<u8> {
let mut key = generate_blowfish_key(None);
Expand All @@ -191,8 +196,17 @@ impl Shutdown for ClientHandler {

#[async_trait]
impl PacketSender for ClientHandler {
fn encryption(&self) -> Option<&Encryption> {
self.blowfish.as_ref()
async fn encrypt(&self, bytes: &mut [u8]) -> anyhow::Result<()> {
if let Some(bf) = self.blowfish.as_ref() {
let size = bytes.len();
Encryption::append_checksum(&mut bytes[2..size]);
bf.lock().await.encrypt(&mut bytes[2..size]);
}
Ok(())
}

fn is_encryption_enabled(&self) -> bool {
self.blowfish.is_some()
}

async fn get_stream_writer_mut(&self) -> &Arc<Mutex<dyn AsyncWrite + Unpin + Send>> {
Expand Down Expand Up @@ -291,13 +305,9 @@ impl PacketHandler for ClientHandler {

#[instrument(skip(self, bytes))]
async fn on_receive_bytes(&mut self, _: usize, bytes: &mut [u8]) -> Result<(), Error> {
if let Some(blowfish) = self.encryption() {
blowfish.decrypt(bytes)?;
if !Encryption::verify_checksum(bytes) {
bail!("Can not verify check sum.");
}
if let Some(blowfish) = self.blowfish.as_ref() {
blowfish.lock().await.decrypt(bytes)?;
}

let handler = build_client_packet(bytes)?;
handler.handle(self).await
}
Expand Down Expand Up @@ -443,8 +453,12 @@ mod tests {

#[async_trait]
impl PacketSender for TestPacketSender {
fn encryption(&self) -> Option<&Encryption> {
None
async fn encrypt(&self, _: &mut [u8]) -> anyhow::Result<()> {
Ok(())
}

fn is_encryption_enabled(&self) -> bool {
false
}

async fn get_stream_writer_mut(&self) -> &Arc<Mutex<dyn AsyncWrite + Send + Unpin>> {
Expand Down
2 changes: 1 addition & 1 deletion game/src/cp_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub fn build_client_packet(
SelectChar::PACKET_ID => Ok(Box::new(SelectChar::read(packet_body)?)),
0xD0 => build_ex_client_packet(packet_body),
_ => {
error!("Unknown GS packet ID:0x{:02X}", data[0]);
error!("Unknown GS packet ID: 0x{:02X}", data[0]);
Ok(Box::new(NoOp::read(data)?))
}
}
Expand Down
11 changes: 9 additions & 2 deletions game/src/ls_thread/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,15 @@ impl Shutdown for LoginHandler {

#[async_trait]
impl PacketSender for LoginHandler {
fn encryption(&self) -> Option<&Encryption> {
Some(&self.blowfish)
async fn encrypt(&self, bytes: &mut [u8]) -> anyhow::Result<()> {
let size = bytes.len();
Encryption::append_checksum(&mut bytes[2..size]);
self.blowfish.encrypt(&mut bytes[2..size]);
Ok(())
}

fn is_encryption_enabled(&self) -> bool {
true
}

async fn get_stream_writer_mut(&self) -> &Arc<Mutex<dyn AsyncWrite + Unpin + Send>> {
Expand Down
6 changes: 3 additions & 3 deletions game/src/packets/from_client/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::packets::to_client::ProtocolResponse;
use crate::packets::HandleablePacket;
use anyhow::bail;
use async_trait::async_trait;
use l2_core::crypt::login::Encryption;
use l2_core::crypt::game::GameClientEncryption;
use l2_core::shared_packets::common::ReadablePacket;
use l2_core::shared_packets::read::ReadablePacketBuffer;
use l2_core::shared_packets::write::SendablePacketBuffer;
Expand Down Expand Up @@ -33,7 +33,7 @@ impl ReadablePacket for ProtocolVersion {
impl HandleablePacket for ProtocolVersion {
type HandlerType = ClientHandler;
async fn handle(&self, handler: &mut Self::HandlerType) -> anyhow::Result<()> {
let controller = handler.get_controller();
let controller = handler.get_controller().clone();
let cfg = controller.get_cfg();
if let Err(e) = handler.set_protocol(self.version) {
handler
Expand All @@ -44,7 +44,7 @@ impl HandleablePacket for ProtocolVersion {

let key_bytes = ClientHandler::generate_key();
if cfg.enable_encryption {
let key = Encryption::from_u8_key(&key_bytes);
let key = GameClientEncryption::new(&key_bytes)?;
handler.set_encryption(Some(key));
}
handler
Expand Down
2 changes: 1 addition & 1 deletion l2-core/src/config/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub struct LoginServer {
pub name: String,
pub blowfish_key: String,
pub runtime: Option<Runtime>,
pub auto_registration: bool,
#[serde(deserialize_with = "validate_allowed_gs_keys")]
pub allowed_gs: Option<Vec<BigInt>>,
pub listeners: Listeners,
Expand Down Expand Up @@ -93,6 +92,7 @@ pub struct Client {
pub timeout: u8,
pub show_licence: bool,
pub enable_cmdline_login: bool,
pub auto_create_accounts: bool,
}

#[derive(Debug, Clone, Deserialize)]
Expand Down
78 changes: 78 additions & 0 deletions l2-core/src/crypt/game.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::crypt::BLOWFISH_KEY_SIZE;
use crate::errors::Packet;
use anyhow::bail;

#[derive(Debug)]
pub struct GameClientEncryption {
in_key: [u8; BLOWFISH_KEY_SIZE],
out_key: [u8; BLOWFISH_KEY_SIZE],
is_enabled: bool,
}
impl GameClientEncryption {
pub fn new(key: &[u8]) -> anyhow::Result<GameClientEncryption> {
if key.len() != BLOWFISH_KEY_SIZE {
bail!(Packet::SendPacketError);
}
let mut key_array: [u8; 16] = [0; 16];
key_array.clone_from_slice(&key[..16]);
//because of implicit copy, these will be two different arrays
Ok(Self {
in_key: key_array, //copy 1
out_key: key_array, //copy 2
is_enabled: false,
})
}
#[allow(clippy::cast_possible_truncation)]
pub fn decrypt(&mut self, data: &mut [u8]) -> Result<(), Packet> {
if !self.is_enabled {
self.is_enabled = true;
return Ok(());
}
let mut x_or = 0u8;
let size = data.len();
for i in 0..size {
let encrypted = data[i]; // Equivalent to Byte.toUnsignedInt in Java
data[i] = encrypted ^ self.in_key[i & 15] ^ x_or; // XOR operation and write byte back
x_or = encrypted;
}

// Shift key
let mut old = u32::from(self.in_key[8]);
old |= u32::from(self.in_key[9]) << 8;
old |= u32::from(self.in_key[10]) << 16;
old |= u32::from(self.in_key[11]) << 24;

old += size as u32;
self.in_key[8] = (old & 0xff) as u8;
self.in_key[9] = ((old >> 8) & 0xff) as u8;
self.in_key[10] = ((old >> 16) & 0xff) as u8;
self.in_key[11] = ((old >> 24) & 0xff) as u8;
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
pub fn encrypt(&mut self, data: &mut [u8]) {
if !self.is_enabled {
self.is_enabled = true;
return;
}
let mut encrypted = 0;
let size = data.len();
for i in 0..size {
let raw = u32::from(data[i]);
encrypted ^= raw ^ u32::from(self.out_key[i & 0x0f]);
data[i] = encrypted as u8;
}

// Shift key
let mut old = u32::from(self.out_key[8]);
old |= u32::from(self.out_key[9]) << 8;
old |= u32::from(self.out_key[10]) << 16;
old |= u32::from(self.out_key[11]) << 24;
old += size as u32;

self.out_key[8] = (old & 0xff) as u8;
self.out_key[9] = ((old >> 8) & 0xff) as u8;
self.out_key[10] = ((old >> 16) & 0xff) as u8;
self.out_key[11] = ((old >> 24) & 0xff) as u8;
}
}
2 changes: 2 additions & 0 deletions l2-core/src/crypt/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod login;
pub mod rsa;
pub mod game;

pub static STATIC_BLOWFISH_KEY: [u8; 16] = [
154, 125, 7, 25, 132, 212, 137, 240, 220, 37, 6, 180, 21, 131, 47, 197,
];
Expand Down
17 changes: 9 additions & 8 deletions l2-core/src/traits/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::crypt::login::Encryption;
use crate::dto;
use crate::shared_packets::common::SendablePacket;
use crate::traits::Shutdown;
Expand Down Expand Up @@ -28,15 +27,13 @@ pub trait OutboundHandler {
#[async_trait]
pub trait PacketSender: Send + Sync + Debug {
async fn send_packet(&self, mut packet: Box<dyn SendablePacket>) -> Result<(), Error> {
self.send_bytes(packet.get_bytes(self.encryption().is_some()))
self.send_bytes(packet.get_bytes(self.is_encryption_enabled()))
.await?;
Ok(())
}
async fn send_bytes(&self, bytes: &mut [u8]) -> Result<(), Error> {
let size = bytes.len();
if let Some(blowfish) = self.encryption() {
Encryption::append_checksum(&mut bytes[2..size]);
blowfish.encrypt(&mut bytes[2..size]);
if self.is_encryption_enabled() {
self.encrypt(bytes).await?;
}
self.get_stream_writer_mut()
.await
Expand All @@ -46,7 +43,10 @@ pub trait PacketSender: Send + Sync + Debug {
.await?;
Ok(())
}
fn encryption(&self) -> Option<&Encryption>;

#[allow(clippy::missing_errors_doc)]
async fn encrypt(&self, bytes: &mut [u8]) -> anyhow::Result<()>;
fn is_encryption_enabled(&self) -> bool;
async fn get_stream_writer_mut(&self) -> &Arc<Mutex<dyn AsyncWrite + Send + Unpin>>;
}
#[async_trait]
Expand Down Expand Up @@ -109,7 +109,8 @@ pub trait PacketHandler: PacketSender + Shutdown + Send + Sync + Debug {
match read_result {
Ok((0, _)) => {
self.on_disconnect().await;
bail!("No bytes received.");
// it's okay that we received 0 bytes, client wants to close socket
break;
}
Ok((bytes_read, mut data)) => {
if let Err(e) = self.on_receive_bytes(bytes_read, &mut data).await {
Expand Down
7 changes: 5 additions & 2 deletions login/src/client_thread/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,11 @@ impl PacketHandler for Client {

#[async_trait]
impl PacketSender for Client {
fn encryption(&self) -> Option<&Encryption> {
None
async fn encrypt(&self, _: &mut [u8]) -> anyhow::Result<()> {
Ok(())
}
fn is_encryption_enabled(&self) -> bool {
false
}

async fn get_stream_writer_mut(&self) -> &Arc<Mutex<dyn AsyncWrite + Send + Unpin>> {
Expand Down
11 changes: 9 additions & 2 deletions login/src/gs_thread/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,15 @@ impl Shutdown for GameServer {

#[async_trait]
impl PacketSender for GameServer {
fn encryption(&self) -> Option<&Encryption> {
Some(&self.blowfish)
async fn encrypt(&self, bytes: &mut [u8]) -> anyhow::Result<()> {
let size = bytes.len();
Encryption::append_checksum(&mut bytes[2..size]);
self.blowfish.encrypt(&mut bytes[2..size]);
Ok(())
}

fn is_encryption_enabled(&self) -> bool {
true
}

async fn get_stream_writer_mut(&self) -> &Arc<Mutex<dyn AsyncWrite + Send + Unpin>> {
Expand Down
10 changes: 4 additions & 6 deletions login/src/packet/cp_factory.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use anyhow::bail;
use crate::client_thread::ClientHandler;
use crate::packet::from_client::{
RequestAuthGG, RequestAuthLogin, RequestGSLogin, RequestServerList,
RequestAuthCMDLogin, RequestAuthGG, RequestAuthLogin, RequestGSLogin, RequestServerList,
};
use crate::packet::HandleablePacket;
use anyhow::bail;
use l2_core::crypt::rsa::ScrambledRSAKeyPair;
use l2_core::shared_packets::common::ReadablePacket;

Expand Down Expand Up @@ -32,11 +32,9 @@ pub fn build_client_packet(
Ok(Box::new(RequestAuthLogin::read(&decrypted)?))
}
RequestAuthGG::PACKET_ID => Ok(Box::new(RequestAuthGG::read(packet_body)?)),
// 0x0B => Some(), //cmd login
RequestAuthCMDLogin::PACKET_ID => Ok(Box::new(RequestAuthLogin::read(packet_body)?)), //cmd login
RequestGSLogin::PACKET_ID => Ok(Box::new(RequestGSLogin::read(packet_body)?)),
RequestServerList::PACKET_ID => {
Ok(Box::new(RequestServerList::read(packet_body)?))
}
RequestServerList::PACKET_ID => Ok(Box::new(RequestServerList::read(packet_body)?)),
// 0x0E => Some(LoginClientOpcodes::RequestPiAgreementCheck),
// 0x0F => Some(LoginClientOpcodes::RequestPiAgreement),
_ => {
Expand Down
Loading

0 comments on commit 5ff5bca

Please sign in to comment.