mirror of
https://github.com/yoncodes/BD2PS.git
synced 2026-08-04 22:02:15 +02:00
pushing
This commit is contained in:
18
crypto/Cargo.toml
Normal file
18
crypto/Cargo.toml
Normal 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
121
crypto/src/aestools.rs
Normal 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
2
crypto/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod aestools;
|
||||
pub mod network;
|
||||
174
crypto/src/network.rs
Normal file
174
crypto/src/network.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user