mirror of
https://github.com/beerpiss/saekawa.git
synced 2024-11-27 17:00:50 +01:00
feat: Allow requests to title server to stay encrypted (#1)
* initial encryption work still need to figure out how to decrypt the requests. i don't even have an idea how long the key is. * feat: encryption support
This commit is contained in:
parent
183995a002
commit
4ce49973a5
4
.gitignore
vendored
4
.gitignore
vendored
@ -7,10 +7,6 @@
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
|
1141
Cargo.lock
generated
Normal file
1141
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "saekawa"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["beerpsi <lacvtg.a1.2023@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "0BSD"
|
||||
@ -16,19 +16,24 @@ codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
winapi = { version = "0.3.9", features = ["winhttp", "minwindef", "consoleapi"] }
|
||||
serde = { version = "1.0.192", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
aes = "0.8.3"
|
||||
anyhow = "1.0.75"
|
||||
cbc = "0.1.2"
|
||||
chrono = "0.4.31"
|
||||
confy = "0.5.1"
|
||||
env_logger = "0.10.1"
|
||||
faster-hex = "0.8.1"
|
||||
flate2 = "1.0.28"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.20"
|
||||
url = "2.4.1"
|
||||
chrono = "0.4.31"
|
||||
env_logger = "0.10.1"
|
||||
retour = { version = "0.3.1", features = ["static-detour"] }
|
||||
widestring = "1.0.2"
|
||||
flate2 = "1.0.28"
|
||||
ureq = { version = "2.8.0", features = ["json"] }
|
||||
num_enum = "0.7.1"
|
||||
pbkdf2 = "0.12.2"
|
||||
retour = { version = "0.3.1", features = ["static-detour"] }
|
||||
serde = { version = "1.0.192", features = ["derive"] }
|
||||
serde-aux = "4.2.0"
|
||||
serde_json = "1.0.108"
|
||||
sha1 = "0.10.6"
|
||||
ureq = { version = "2.8.0", features = ["json"] }
|
||||
url = "2.4.1"
|
||||
widestring = "1.0.2"
|
||||
winapi = { version = "0.3.9", features = ["winhttp", "minwindef", "consoleapi"] }
|
||||
|
@ -20,11 +20,13 @@ edit your segatools game.bat to look like the green line:
|
||||
This hook requires the game's network communications to be decrypted, which can be done
|
||||
by patching the game binary with your preferred patcher.
|
||||
|
||||
Support for encrypted requests might come soon.
|
||||
If you choose not to patch, you will need to obtain the necessary keys and provide them
|
||||
in `saekawa.toml`.
|
||||
|
||||
### Credits
|
||||
- Adam Thibert ([adamaq01](https://github.com/adamaq01)). A lot of the code was shamelessly lifted from his
|
||||
[Mikado](https://github.com/adamaq01/Mikado), a similar hook for SDVX.
|
||||
- Adam Thibert ([adamaq01](https://github.com/adamaq01)). A lot of the code was
|
||||
shamelessly lifted from his [Mikado](https://github.com/adamaq01/Mikado), a similar
|
||||
hook for SDVX.
|
||||
|
||||
### License
|
||||
0BSD
|
@ -15,6 +15,19 @@ timeout = 3000
|
||||
# example: whitelist = ["00001111222233334444"]
|
||||
whitelist = []
|
||||
|
||||
[crypto]
|
||||
# Since CRYSTAL+, the game employs network encryption. If you do not wish to
|
||||
# patch your game to disable encryption, you will need to fill in these values.
|
||||
#
|
||||
# All values are in hex strings.
|
||||
key = ""
|
||||
iv = ""
|
||||
salt = ""
|
||||
|
||||
# Number of PBKDF2 iterations to hash the endpoint URL. Set to 70 for CHUNITHM SUN
|
||||
# and newer, and 44 for older versions.
|
||||
iterations = 70
|
||||
|
||||
[tachi]
|
||||
# Tachi instance base URL
|
||||
base_url = 'https://kamaitachi.xyz/'
|
||||
|
@ -8,6 +8,7 @@ use std::path::Path;
|
||||
pub struct Configuration {
|
||||
pub general: GeneralConfiguration,
|
||||
pub cards: CardsConfiguration,
|
||||
pub crypto: CryptoConfiguration,
|
||||
pub tachi: TachiConfiguration,
|
||||
}
|
||||
|
||||
@ -57,6 +58,25 @@ pub struct CardsConfiguration {
|
||||
pub whitelist: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CryptoConfiguration {
|
||||
#[serde(with = "faster_hex")]
|
||||
pub key: Vec<u8>,
|
||||
|
||||
#[serde(with = "faster_hex")]
|
||||
pub iv: Vec<u8>,
|
||||
|
||||
#[serde(with = "faster_hex")]
|
||||
pub salt: Vec<u8>,
|
||||
|
||||
#[serde(default = "default_iterations")]
|
||||
pub iterations: u32,
|
||||
}
|
||||
|
||||
fn default_iterations() -> u32 {
|
||||
70
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct TachiConfiguration {
|
||||
pub base_url: String,
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::{fmt::Debug, io::Read};
|
||||
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
use anyhow::{anyhow, Result};
|
||||
use flate2::read::ZlibDecoder;
|
||||
use log::debug;
|
||||
@ -14,7 +15,9 @@ use winapi::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::CONFIGURATION;
|
||||
use crate::{CONFIGURATION, UPSERT_USER_ALL_API_ENCRYPTED};
|
||||
|
||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||
|
||||
pub fn request_agent() -> ureq::Agent {
|
||||
let timeout = CONFIGURATION.general.timeout;
|
||||
@ -79,6 +82,7 @@ where
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Queries a HINTERNET handle for its URL, then return the result.
|
||||
pub fn read_hinternet_url(handle: HINTERNET) -> Result<String> {
|
||||
let mut buf_length = 255;
|
||||
let mut buffer = [0u16; 255];
|
||||
@ -122,29 +126,84 @@ pub fn read_hinternet_url(handle: HINTERNET) -> Result<String> {
|
||||
}
|
||||
|
||||
let ec = unsafe { GetLastError() };
|
||||
return Err(anyhow!("Could not get URL from HINTERNET handle: {ec}"));
|
||||
Err(anyhow!("Could not get URL from HINTERNET handle: {ec}"))
|
||||
}
|
||||
|
||||
pub fn read_potentially_deflated_buffer(buf: *const u8, len: usize) -> Result<String> {
|
||||
pub fn read_slice(buf: *const u8, len: usize) -> Result<Vec<u8>> {
|
||||
let mut slice = unsafe { std::slice::from_raw_parts(buf, len) };
|
||||
let mut ret = Vec::with_capacity(len);
|
||||
|
||||
slice.read_to_end(&mut ret)?;
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn read_maybe_compressed_buffer(buf: impl AsRef<[u8]>) -> Result<String> {
|
||||
let mut ret = String::new();
|
||||
|
||||
let mut decoder = ZlibDecoder::new(slice);
|
||||
let mut decoder = ZlibDecoder::new(buf.as_ref());
|
||||
let zlib_result = decoder.read_to_string(&mut ret);
|
||||
if zlib_result.is_ok() {
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
ret.clear();
|
||||
let result = slice.read_to_string(&mut ret);
|
||||
let result = buf.as_ref().read_to_string(&mut ret);
|
||||
if result.is_ok() {
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
// Unwrapping here is fine, if result was Ok we wouldn't reach this place.
|
||||
Err(anyhow!(
|
||||
"Could not decode contents of buffer as both DEFLATE-compressed ({:#}) and plaintext ({:#}) UTF-8 string.",
|
||||
zlib_result.err().expect("This shouldn't happen, if Result was Ok the string should have been returned earlier."),
|
||||
result.err().expect("This shouldn't happen, if Result was Ok the string should have been returned earlier."),
|
||||
zlib_result.expect_err("This shouldn't happen, if Result was Ok the string should have been returned earlier."),
|
||||
result.expect_err("This shouldn't happen, if Result was Ok the string should have been returned earlier."),
|
||||
))
|
||||
}
|
||||
|
||||
/// Determine if we're looking at the UpsertUserAllApi endpoint,
|
||||
/// which is the endpoint that contains our scores.
|
||||
pub fn is_upsert_user_all_endpoint(endpoint: &str) -> bool {
|
||||
if endpoint == "UpsertUserAllApi" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if UPSERT_USER_ALL_API_ENCRYPTED
|
||||
.as_ref()
|
||||
.is_some_and(|v| v == endpoint)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Determine if it is an encrypted endpoint by checking if the endpoint
|
||||
/// is exactly 32 characters long, and consists of all hex characters.
|
||||
///
|
||||
/// While this may trigger false positives, this should not happen as long
|
||||
/// as CHUNITHM title APIs keep their `{method}{object}Api` endpoint
|
||||
/// convention.
|
||||
pub fn is_encrypted_endpoint(endpoint: &str) -> bool {
|
||||
if endpoint.len() != 32 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lazy way to check if all digits are hexadecimal
|
||||
if u128::from_str_radix(endpoint, 16).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn decrypt_aes256_cbc(
|
||||
body: &mut [u8],
|
||||
key: impl AsRef<[u8]>,
|
||||
iv: impl AsRef<[u8]>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cipher = Aes256CbcDec::new_from_slices(key.as_ref(), iv.as_ref())?;
|
||||
Ok(cipher
|
||||
.decrypt_padded_mut::<Pkcs7>(body)
|
||||
.map_err(|err| anyhow!("{:#}", err))?
|
||||
.to_owned())
|
||||
}
|
||||
|
21
src/lib.rs
21
src/lib.rs
@ -4,8 +4,10 @@ mod log;
|
||||
mod saekawa;
|
||||
mod types;
|
||||
|
||||
use ::log::error;
|
||||
use ::log::{debug, error};
|
||||
use lazy_static::lazy_static;
|
||||
use pbkdf2::pbkdf2_hmac_array;
|
||||
use sha1::Sha1;
|
||||
use url::Url;
|
||||
use winapi::shared::minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, TRUE};
|
||||
use winapi::um::winnt::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH};
|
||||
@ -44,6 +46,23 @@ lazy_static! {
|
||||
|
||||
result.unwrap().to_string()
|
||||
};
|
||||
pub static ref UPSERT_USER_ALL_API_ENCRYPTED: Option<String> = {
|
||||
if CONFIGURATION.crypto.salt.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let key_bytes = pbkdf2_hmac_array::<Sha1, 16>(
|
||||
b"UpsertUserAllApi",
|
||||
&CONFIGURATION.crypto.salt,
|
||||
CONFIGURATION.crypto.iterations,
|
||||
);
|
||||
|
||||
let key = faster_hex::hex_string(&key_bytes);
|
||||
|
||||
debug!("Running with encryption support: UpsertUserAllApi maps to {key}");
|
||||
|
||||
Some(key)
|
||||
};
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{ffi::CString, ptr};
|
||||
use std::ffi::CString;
|
||||
|
||||
use ::log::{debug, error, info};
|
||||
use anyhow::{anyhow, Result};
|
||||
@ -14,12 +14,15 @@ use winapi::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::{call_tachi, read_hinternet_url, read_potentially_deflated_buffer, request_tachi},
|
||||
helpers::{
|
||||
call_tachi, decrypt_aes256_cbc, is_encrypted_endpoint, is_upsert_user_all_endpoint,
|
||||
read_hinternet_url, read_maybe_compressed_buffer, read_slice, request_tachi,
|
||||
},
|
||||
types::{
|
||||
game::UpsertUserAllRequest,
|
||||
tachi::{ClassEmblem, Difficulty, Import, ImportClasses, ImportScore},
|
||||
},
|
||||
CONFIGURATION, TACHI_IMPORT_URL, TACHI_STATUS_URL,
|
||||
CONFIGURATION, TACHI_IMPORT_URL, TACHI_STATUS_URL, UPSERT_USER_ALL_API_ENCRYPTED,
|
||||
};
|
||||
|
||||
type WinHttpWriteDataFunc = unsafe extern "system" fn(HINTERNET, LPCVOID, DWORD, LPDWORD) -> BOOL;
|
||||
@ -103,19 +106,66 @@ fn winhttpwritedata_hook(
|
||||
};
|
||||
debug!("winhttpwritedata URL: {url}");
|
||||
|
||||
let request_body = match read_potentially_deflated_buffer(
|
||||
lp_buffer as *const u8,
|
||||
dw_number_of_bytes_to_write as usize,
|
||||
) {
|
||||
let Some(endpoint) = url.split('/').last() else {
|
||||
error!("Could not get name of endpoint");
|
||||
return orig();
|
||||
};
|
||||
|
||||
let is_upsert_user_all = is_upsert_user_all_endpoint(endpoint);
|
||||
// Exit early if release mode and the endpoint is not what we're looking for
|
||||
if cfg!(not(debug_assertions)) && !is_upsert_user_all {
|
||||
return orig();
|
||||
}
|
||||
|
||||
let is_encrypted = is_encrypted_endpoint(endpoint);
|
||||
if is_encrypted
|
||||
&& (CONFIGURATION.crypto.key.is_empty()
|
||||
|| CONFIGURATION.crypto.iv.is_empty()
|
||||
|| UPSERT_USER_ALL_API_ENCRYPTED.is_none())
|
||||
{
|
||||
error!("Communications with the server is encrypted, but no keys were provided. Fill in the keys by editing 'saekawa.toml'.");
|
||||
return orig();
|
||||
}
|
||||
|
||||
let mut raw_request_body =
|
||||
match read_slice(lp_buffer as *const u8, dw_number_of_bytes_to_write as usize) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
error!("There was an error reading the request body: {:#}", err);
|
||||
return orig();
|
||||
}
|
||||
};
|
||||
|
||||
debug!("raw request body: {:?}", raw_request_body);
|
||||
|
||||
let request_body_compressed = if is_encrypted {
|
||||
match decrypt_aes256_cbc(
|
||||
&mut raw_request_body,
|
||||
&CONFIGURATION.crypto.key,
|
||||
&CONFIGURATION.crypto.iv,
|
||||
) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
error!("Could not decrypt request: {:#}", err);
|
||||
return orig();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
raw_request_body
|
||||
};
|
||||
|
||||
let request_body = match read_maybe_compressed_buffer(&request_body_compressed[..]) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
error!("There was an error reading the request body: {:#}", err);
|
||||
error!("There was an error decoding the request body: {:#}", err);
|
||||
return orig();
|
||||
}
|
||||
};
|
||||
debug!("winhttpwritedata request body: {request_body}");
|
||||
|
||||
if !url.contains("UpsertUserAllApi") {
|
||||
debug!("decoded request body: {request_body}");
|
||||
|
||||
// Reached in debug mode
|
||||
if !is_upsert_user_all {
|
||||
return orig();
|
||||
}
|
||||
|
||||
@ -197,13 +247,13 @@ fn get_proc_address(module: &str, function: &str) -> Result<*mut __some_function
|
||||
let fun_name = CString::new(function).unwrap();
|
||||
|
||||
let module = unsafe { GetModuleHandleA(module_name.as_ptr()) };
|
||||
if (module as *const c_void) == ptr::null() {
|
||||
if (module as *const c_void).is_null() {
|
||||
let ec = unsafe { GetLastError() };
|
||||
return Err(anyhow!("could not get module handle, error code {ec}"));
|
||||
}
|
||||
|
||||
let addr = unsafe { GetProcAddress(module, fun_name.as_ptr()) };
|
||||
if (addr as *const c_void) == ptr::null() {
|
||||
if (addr as *const c_void).is_null() {
|
||||
let ec = unsafe { GetLastError() };
|
||||
return Err(anyhow!("could not get function address, error code {ec}"));
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ pub struct UserPlaylog {
|
||||
pub is_clear: bool,
|
||||
}
|
||||
|
||||
fn default_judge_heaven<'a>() -> u32 {
|
||||
fn default_judge_heaven() -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user