mirror of
https://github.com/beerpiss/saekawa.git
synced 2024-11-27 17:00:50 +01:00
feat: encryption support
This commit is contained in:
parent
48988828d9
commit
ebca84ceb8
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
30
Cargo.toml
30
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,24 +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"
|
||||
serde-aux = "4.2.0"
|
||||
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"
|
||||
hex = "0.4.3"
|
||||
aes = "0.8.3"
|
||||
cbc = "0.1.2"
|
||||
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
|
@ -3,6 +3,8 @@
|
||||
enable = true
|
||||
# Whether the hook should export your class medals and emblems or not.
|
||||
export_class = true
|
||||
# Whether FAILED should override FULL COMBO and ALL JUSTICE.
|
||||
fail_over_lamp = false
|
||||
# Timeout for web requests, in milliseconds
|
||||
timeout = 3000
|
||||
|
||||
|
@ -51,12 +51,23 @@ pub struct CardsConfiguration {
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CryptoConfiguration {
|
||||
pub key: String,
|
||||
pub iv: String,
|
||||
pub salt: String,
|
||||
#[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,6 +1,8 @@
|
||||
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;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use widestring::U16CString;
|
||||
@ -15,6 +17,8 @@ use winapi::{
|
||||
|
||||
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;
|
||||
let timeout = if timeout > 10000 { 10000 } else { timeout };
|
||||
@ -78,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];
|
||||
@ -124,8 +129,9 @@ pub fn read_hinternet_url(handle: HINTERNET) -> Result<String> {
|
||||
Err(anyhow!("Could not get URL from HINTERNET handle: {ec}"))
|
||||
}
|
||||
|
||||
pub unsafe fn read_potentially_deflated_buffer(buf: *const u8, len: usize) -> Result<Vec<u8>> {
|
||||
let mut slice = std::slice::from_raw_parts(buf, len);
|
||||
/// Read all bytes of a slice into a buffer.
|
||||
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)?;
|
||||
@ -133,23 +139,52 @@ pub unsafe fn read_potentially_deflated_buffer(buf: *const u8, len: usize) -> Re
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn is_upsert_user_all_endpoint(endpoint: &str, is_encrypted: bool) -> bool {
|
||||
pub fn read_maybe_compressed_buffer(buf: impl AsRef<[u8]>) -> Result<String> {
|
||||
let mut ret = String::new();
|
||||
|
||||
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 = buf.as_ref().read_to_string(&mut ret);
|
||||
if result.is_ok() {
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"Could not decode contents of buffer as both DEFLATE-compressed ({:#}) and plaintext ({:#}) UTF-8 string.",
|
||||
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 is_encrypted && endpoint == UPSERT_USER_ALL_API_ENCRYPTED.as_str() {
|
||||
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 it's a hex string of 32 characters, we're most likely dealing
|
||||
// with title server encryption. Chance of false positive is very low,
|
||||
// but technically not 0.
|
||||
|
||||
if endpoint.len() != 32 {
|
||||
return false;
|
||||
}
|
||||
@ -161,3 +196,15 @@ pub fn is_encrypted_endpoint(endpoint: &str) -> bool {
|
||||
|
||||
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())
|
||||
}
|
||||
|
27
src/lib.rs
27
src/lib.rs
@ -4,7 +4,7 @@ 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;
|
||||
@ -46,27 +46,22 @@ lazy_static! {
|
||||
|
||||
result.unwrap().to_string()
|
||||
};
|
||||
pub static ref UPSERT_USER_ALL_API_ENCRYPTED: String = {
|
||||
pub static ref UPSERT_USER_ALL_API_ENCRYPTED: Option<String> = {
|
||||
if CONFIGURATION.crypto.salt.is_empty() {
|
||||
// return a bullshit value
|
||||
return "ffffffffffffffffffffffffffffffff".to_string();
|
||||
return None;
|
||||
}
|
||||
|
||||
let salt = match hex::decode(&CONFIGURATION.crypto.salt) {
|
||||
Ok(salt) => salt,
|
||||
Err(err) => {
|
||||
error!("Could not parse salt as hex string: {:#}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let key = pbkdf2_hmac_array::<Sha1, 16>(
|
||||
let key_bytes = pbkdf2_hmac_array::<Sha1, 16>(
|
||||
b"UpsertUserAllApi",
|
||||
&salt,
|
||||
CONFIGURATION.crypto.iterations
|
||||
&CONFIGURATION.crypto.salt,
|
||||
CONFIGURATION.crypto.iterations,
|
||||
);
|
||||
|
||||
hex::encode(key)
|
||||
let key = faster_hex::hex_string(&key_bytes);
|
||||
|
||||
debug!("Running with encryption support: UpsertUserAllApi maps to {key}");
|
||||
|
||||
Some(key)
|
||||
};
|
||||
}
|
||||
|
||||
|
107
src/saekawa.rs
107
src/saekawa.rs
@ -1,9 +1,7 @@
|
||||
use std::{ffi::CString, io::Read, ptr};
|
||||
use std::ffi::CString;
|
||||
|
||||
use ::log::{debug, error, info};
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||
use anyhow::{anyhow, Result};
|
||||
use flate2::read::ZlibDecoder;
|
||||
use retour::static_detour;
|
||||
use winapi::{
|
||||
ctypes::c_void,
|
||||
@ -15,18 +13,16 @@ use winapi::{
|
||||
},
|
||||
};
|
||||
|
||||
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
self, call_tachi, is_encrypted_endpoint, is_upsert_user_all_endpoint, read_hinternet_url,
|
||||
read_potentially_deflated_buffer,
|
||||
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, 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;
|
||||
@ -40,8 +36,7 @@ pub fn hook_init() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let resp: serde_json::Value =
|
||||
helpers::request_tachi("GET", TACHI_STATUS_URL.as_str(), None::<()>)?;
|
||||
let resp: serde_json::Value = request_tachi("GET", TACHI_STATUS_URL.as_str(), None::<()>)?;
|
||||
let user_id = resp["body"]["whoami"]
|
||||
.as_u64()
|
||||
.ok_or(anyhow::anyhow!("Couldn't parse user from Tachi response"))?;
|
||||
@ -55,9 +50,7 @@ pub fn hook_init() -> Result<()> {
|
||||
};
|
||||
|
||||
unsafe {
|
||||
DetourWriteData.initialize(winhttpwritedata, move |a, b, c, d| {
|
||||
winhttpwritedata_hook(a, b, c, d)
|
||||
})?;
|
||||
DetourWriteData.initialize(winhttpwritedata, winhttpwritedata_hook)?;
|
||||
|
||||
DetourWriteData.enable()?;
|
||||
};
|
||||
@ -76,7 +69,7 @@ pub fn hook_release() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn winhttpwritedata_hook(
|
||||
fn winhttpwritedata_hook(
|
||||
h_request: HINTERNET,
|
||||
lp_buffer: LPCVOID,
|
||||
dw_number_of_bytes_to_write: DWORD,
|
||||
@ -84,7 +77,7 @@ unsafe fn winhttpwritedata_hook(
|
||||
) -> BOOL {
|
||||
debug!("hit winhttpwritedata");
|
||||
|
||||
let orig = || {
|
||||
let orig = || unsafe {
|
||||
DetourWriteData.call(
|
||||
h_request,
|
||||
lp_buffer,
|
||||
@ -102,59 +95,55 @@ unsafe fn winhttpwritedata_hook(
|
||||
};
|
||||
debug!("winhttpwritedata URL: {url}");
|
||||
|
||||
let endpoint = match url.split('/').last() {
|
||||
Some(endpoint) => endpoint,
|
||||
None => {
|
||||
error!("Could not get name of endpoint");
|
||||
return orig();
|
||||
}
|
||||
let Some(endpoint) = url.split('/').last() else {
|
||||
error!("Could not get name of endpoint");
|
||||
return orig();
|
||||
};
|
||||
|
||||
let is_encrypted = is_encrypted_endpoint(endpoint);
|
||||
if is_encrypted
|
||||
&& (CONFIGURATION.crypto.key.is_empty()
|
||||
|| CONFIGURATION.crypto.iv.is_empty()
|
||||
|| CONFIGURATION.crypto.salt.is_empty())
|
||||
{
|
||||
error!("Communications with the server is encrypted, but no keys were provided. Fill in the keys by editing 'saekawa.toml'.");
|
||||
return orig();
|
||||
}
|
||||
|
||||
let is_upsert_user_all = is_upsert_user_all_endpoint(endpoint, is_encrypted);
|
||||
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 raw_request_body = match read_potentially_deflated_buffer(
|
||||
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();
|
||||
}
|
||||
};
|
||||
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 request_body_decrypted = if is_encrypted {
|
||||
// TODO: Decrypt
|
||||
Vec::new()
|
||||
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 mut request_body_bytes = Vec::with_capacity(request_body_decrypted.len());
|
||||
let mut decoder = ZlibDecoder::new(&request_body_decrypted[..]);
|
||||
if let Err(err) = decoder.read_to_end(&mut request_body_bytes) {
|
||||
debug!(
|
||||
"Could not inflate request body, treating it as uncompressed: {:#}",
|
||||
err
|
||||
);
|
||||
request_body_bytes = request_body_decrypted;
|
||||
}
|
||||
|
||||
let request_body = match String::from_utf8(request_body_bytes) {
|
||||
let request_body = match read_maybe_compressed_buffer(&request_body_compressed[..]) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
error!("There was an error decoding the request body: {:#}", err);
|
||||
@ -162,7 +151,7 @@ unsafe fn winhttpwritedata_hook(
|
||||
}
|
||||
};
|
||||
|
||||
debug!("winhttpwritedata request body: {request_body}");
|
||||
debug!("decoded request body: {request_body}");
|
||||
|
||||
// Reached in debug mode
|
||||
if !is_upsert_user_all {
|
||||
@ -245,13 +234,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