This commit is contained in:
yoncodes
2025-10-28 21:21:28 -04:00
commit e58fd190b4
2962 changed files with 3063199 additions and 0 deletions

18
crypto/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "crypto"
edition.workspace = true
version.workspace = true
[dependencies]
aes.workspace = true
base64.workspace = true
once_cell.workspace = true
sha1.workspace = true
cbc.workspace = true
serde.workspace = true
prost.workspace = true
chrono.workspace = true
common.workspace = true
tracing.workspace = true
thiserror.workspace = true
serde_json.workspace = true

121
crypto/src/aestools.rs Normal file
View File

@@ -0,0 +1,121 @@
use aes::Aes256;
use base64::{engine::general_purpose, Engine as _};
use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use once_cell::sync::Lazy;
use sha1::{Digest, Sha1};
use std::collections::HashSet;
use std::time::{SystemTime, UNIX_EPOCH};
type Aes256CbcEnc = cbc::Encryptor<Aes256>;
type Aes256CbcDec = cbc::Decryptor<Aes256>;
pub const ENCRYPT_FIX_KEY: &str = "abcdefghijkrstuv024680wxyzlmnopq";
pub const DEFAULT_KEY: &str = "688370f00a38e21a6ca65ec2d7c38a7c";
const SALT: &str = "salt_alone";
static ZERO_IV: [u8; 16] = [0u8; 16];
static ENCRYPT_FIX_PACKET_LIST: Lazy<HashSet<&'static str>> = Lazy::new(|| {
HashSet::from(["LoginUser", "JoinUser"]) // double-Base64 + AES
});
static NON_ENCRYPT_PACKET_LIST: Lazy<HashSet<&'static str>> = Lazy::new(|| {
HashSet::from([
"MaintenanceInfo",
"ServerInfo",
"ServerNowTime",
"NoticeInfo",
])
});
pub enum EncryptionKey {
Fixed, // LoginUser / JoinUser
Default, // other encrypted routes
}
pub struct AesTools;
impl AesTools {
pub fn get_key_for_route(route_name: &str) -> Option<EncryptionKey> {
let route = route_name.trim_start_matches('/').to_string();
if ENCRYPT_FIX_PACKET_LIST.contains(route.as_str()) {
Some(EncryptionKey::Fixed)
} else if NON_ENCRYPT_PACKET_LIST.contains(route.as_str()) {
None
} else {
Some(EncryptionKey::Default)
}
}
/// Encode outbound protobuf payload for a route
pub fn encode_packet(route_name: &str, protobuf_bytes: &[u8]) -> String {
match Self::get_key_for_route(route_name) {
Some(EncryptionKey::Fixed) => {
// Base64( AES( Base64(proto) ) )
let inner_b64 = general_purpose::STANDARD.encode(protobuf_bytes);
Self::aes_encrypt_raw_b64(inner_b64.as_bytes(), ENCRYPT_FIX_KEY)
}
Some(EncryptionKey::Default) => {
// Base64( AES(proto) )
//Self::aes_encrypt_raw_b64(protobuf_bytes, DEFAULT_KEY)
// Base64( AES( Base64(proto) ) )
let inner_b64 = general_purpose::STANDARD.encode(protobuf_bytes);
Self::aes_encrypt_raw_b64(inner_b64.as_bytes(), DEFAULT_KEY)
}
None => {
// Base64(proto)
general_purpose::STANDARD.encode(protobuf_bytes)
}
}
}
/// Decode inbound body → protobuf bytes
pub fn decrypt_packet(route_name: &str, body_b64: &str) -> Result<Vec<u8>, ()> {
match Self::get_key_for_route(route_name) {
Some(EncryptionKey::Fixed) => {
let decrypted = Self::aes_decrypt_raw(body_b64, ENCRYPT_FIX_KEY).map_err(|_| ())?;
let inner_b64 = String::from_utf8(decrypted).map_err(|_| ())?;
general_purpose::STANDARD.decode(inner_b64).map_err(|_| ())
}
Some(EncryptionKey::Default) => {
//Self::aes_decrypt_raw(body_b64, DEFAULT_KEY)
let decrypted = Self::aes_decrypt_raw(body_b64, DEFAULT_KEY).map_err(|_| ())?;
let inner_b64 = String::from_utf8(decrypted).map_err(|_| ())?;
general_purpose::STANDARD.decode(inner_b64).map_err(|_| ())
}
None => general_purpose::STANDARD.decode(body_b64).map_err(|_| ()),
}
}
pub fn aes_encrypt_raw_b64(plaintext: &[u8], key_str: &str) -> String {
let cipher = Aes256CbcEnc::new_from_slices(key_str.as_bytes(), &ZERO_IV).unwrap();
let encrypted = cipher.encrypt_padded_vec_mut::<Pkcs7>(plaintext);
general_purpose::STANDARD.encode(encrypted)
}
pub fn aes_decrypt_raw(cipher_b64: &str, key_str: &str) -> Result<Vec<u8>, ()> {
let decoded = general_purpose::STANDARD
.decode(cipher_b64)
.map_err(|_| ())?;
let cipher = Aes256CbcDec::new_from_slices(key_str.as_bytes(), &ZERO_IV).map_err(|_| ())?;
cipher
.decrypt_padded_vec_mut::<Pkcs7>(&decoded)
.map_err(|_| ())
}
/// SHA-1 hash with static salt suffix
pub fn sha1_hash(input: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(format!("{input}{SALT}"));
format!("{:x}", hasher.finalize())
}
/// Millisecond-precision timestamp
pub fn current_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64
}
}

2
crypto/src/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod aestools;
pub mod network;

174
crypto/src/network.rs Normal file
View File

@@ -0,0 +1,174 @@
use crate::aestools::{AesTools, EncryptionKey, DEFAULT_KEY, ENCRYPT_FIX_KEY};
use base64::{engine::general_purpose, Engine as _};
use chrono::Utc;
use prost::Message;
use serde::{Deserialize, Serialize};
use tracing::{error, info};
#[derive(Debug, thiserror::Error)]
pub enum PacketError {
#[error("empty request body")]
EmptyBody,
#[error("decryption failed")]
DecryptionFailed,
#[error("base64 decode failed: {0}")]
Base64Error(#[from] base64::DecodeError),
#[error("protobuf decode failed: {0}")]
ProtobufError(#[from] prost::DecodeError),
#[error("utf8 decode failed")]
Utf8Error,
}
/// Parse and decrypt incoming packet data
pub fn parse_packet<M: Message + Default>(route_name: &str, body: &str) -> Result<M, PacketError> {
if body.trim().is_empty() {
error!("Empty request body for route {}", route_name);
return Err(PacketError::EmptyBody);
}
let key_type = AesTools::get_key_for_route(route_name);
// Try decryption first, fall back to plain base64 if it fails (for batch requests)
let proto_bytes = match key_type {
Some(EncryptionKey::Fixed) => {
info!("Decrypting using ENCRYPT_FIX_KEY for route {}", route_name);
// Try decrypt: Base64(AES(Base64(proto)))
match try_decrypt_double_base64(body, ENCRYPT_FIX_KEY) {
Ok(bytes) => bytes,
Err(_) => {
// Decryption failed, try plain base64 (batch request)
info!("trying plain base64 for route {}", route_name);
general_purpose::STANDARD
.decode(body)
.map_err(|e| PacketError::Base64Error(e))?
}
}
}
Some(EncryptionKey::Default) => {
info!("Decrypting using DEFAULT_KEY for route {}", route_name);
// Try decrypt: Base64(AES(Base64(proto)))
match try_decrypt_double_base64(body, DEFAULT_KEY) {
Ok(bytes) => bytes,
Err(_) => {
// Decryption failed, try plain base64 (batch request)
info!("trying plain base64 for route {}", route_name);
general_purpose::STANDARD
.decode(body)
.map_err(|e| PacketError::Base64Error(e))?
}
}
}
None => {
info!("Non-encrypted route: {}", route_name);
// Direct base64 decode
general_purpose::STANDARD.decode(body)?
}
};
if proto_bytes.is_empty() {
return Err(PacketError::DecryptionFailed);
}
Ok(M::decode(&*proto_bytes)?)
}
/// Helper function to try double base64 + AES decryption
fn try_decrypt_double_base64(body: &str, key: &str) -> Result<Vec<u8>, PacketError> {
// Decrypt: Base64(AES(Base64(proto)))
let decrypted =
AesTools::aes_decrypt_raw(body, key).map_err(|_| PacketError::DecryptionFailed)?;
// Inner layer is base64
let inner_b64 = String::from_utf8(decrypted).map_err(|_| PacketError::Utf8Error)?;
general_purpose::STANDARD
.decode(&inner_b64)
.map_err(|e| PacketError::Base64Error(e))
}
/// Encode and encrypt a protobuf message
pub fn encode_packet<M: Message>(route_name: &str, msg: &M) -> String {
let bytes = msg.encode_to_vec();
AesTools::encode_packet(route_name, &bytes)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BaseResponse {
pub error_type: i32,
pub error_message: String,
pub data: String,
pub length: i32,
pub ip: String,
}
impl BaseResponse {
pub fn success(data: &[u8]) -> Self {
let encoded = general_purpose::STANDARD.encode(data);
Self {
error_type: 0,
error_message: String::new(),
data: encoded,
length: 0, // Game hardcodes this
ip: "127.0.0.1".to_string(),
}
}
pub fn error(error_type: i32) -> Self {
Self {
error_type,
error_message: String::new(),
data: String::new(),
length: 0,
ip: "127.0.0.1".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameResponse {
pub error_type: i32,
pub packet_code: i32,
pub error_message: String,
pub length: i32,
pub data: String,
pub server_now_time: i64,
pub notify: String,
}
impl GameResponse {
pub fn success(route_name: &str, proto_bytes: &[u8], packet_code: i32) -> Self {
let inner_b64 = general_purpose::STANDARD.encode(proto_bytes);
let encrypted = AesTools::encode_packet(route_name, proto_bytes);
Self {
error_type: 0,
packet_code,
error_message: String::new(),
length: inner_b64.len() as i32,
data: encrypted,
server_now_time: Utc::now().timestamp_millis(),
notify: String::new(),
}
}
pub fn error(error_type: i32) -> Self {
Self {
error_type,
packet_code: 0,
error_message: String::new(),
length: 0,
data: String::new(),
server_now_time: 0,
notify: String::new(),
}
}
/// Attach a notify message (plain base64, not encrypted)
pub fn with_notify<M: Message>(mut self, notify: &M) -> Self {
let bytes = notify.encode_to_vec();
self.notify = general_purpose::STANDARD.encode(bytes);
self
}
}