fix(skins): fix authlib-injector routing, add RSA profile signing, and populate userProperties
This commit is contained in:
parent
700b11a3ce
commit
017c9a85f9
4 changed files with 419 additions and 31 deletions
193
Cargo.lock
generated
193
Cargo.lock
generated
|
|
@ -872,6 +872,12 @@ version = "0.23.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "beul"
|
||||
version = "1.0.0"
|
||||
|
|
@ -1358,6 +1364,12 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
|
|
@ -1600,6 +1612,17 @@ version = "0.1.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid 0.9.6",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
|
|
@ -1636,6 +1659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"const-oid 0.9.6",
|
||||
"crypto-common 0.1.7",
|
||||
]
|
||||
|
||||
|
|
@ -1646,7 +1670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid",
|
||||
"const-oid 0.10.2",
|
||||
"crypto-common 0.2.2",
|
||||
"ctutils",
|
||||
]
|
||||
|
|
@ -3462,6 +3486,9 @@ name = "lazy_static"
|
|||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
|
|
@ -3943,6 +3970,22 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand 0.8.8",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
|
|
@ -3979,6 +4022,16 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
|
|
@ -3997,6 +4050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4480,7 +4534,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_app"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
|
@ -4530,7 +4584,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_auth"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
|
@ -4554,7 +4608,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_cluster"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"bytes 1.11.1",
|
||||
"chrono",
|
||||
|
|
@ -4577,7 +4631,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_common"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"interfrost",
|
||||
|
|
@ -4590,7 +4644,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_content"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
|
|
@ -4618,7 +4672,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_core"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"async-stream",
|
||||
|
|
@ -4658,6 +4712,7 @@ dependencies = [
|
|||
"regex",
|
||||
"reqwest 0.13.3",
|
||||
"reqwest-websocket",
|
||||
"rsa",
|
||||
"sanitize-filename",
|
||||
"sentry",
|
||||
"sentry-tracing",
|
||||
|
|
@ -4683,7 +4738,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_db"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"async-tempfile",
|
||||
"chrono",
|
||||
|
|
@ -4698,7 +4753,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_discord"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"discord-rich-presence",
|
||||
|
|
@ -4710,7 +4765,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_events"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
|
@ -4720,7 +4775,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_java"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"astral-tokio-tar",
|
||||
"async-tempfile",
|
||||
|
|
@ -4743,7 +4798,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_macro"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -4752,7 +4807,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_mc"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
|
@ -4774,7 +4829,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_net"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
|
@ -4796,7 +4851,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_polyplus"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"async-tungstenite",
|
||||
"bytes 1.11.1",
|
||||
|
|
@ -4882,7 +4937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4940,6 +4995,15 @@ name = "pathgraph"
|
|||
version = "0.5.0-rc.4"
|
||||
source = "git+https://github.com/marc2332/freya?branch=fix%2Fskip-basic-render-driver#56c6e694084210a8405a8cf9e50a1e236a64efd3"
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -5047,6 +5111,27 @@ dependencies = [
|
|||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||
dependencies = [
|
||||
"der",
|
||||
"pkcs8",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
|
|
@ -5094,7 +5179,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
|||
|
||||
[[package]]
|
||||
name = "polyio"
|
||||
version = "2.5.1-c1"
|
||||
version = "2.5.1-c2"
|
||||
dependencies = [
|
||||
"astral-tokio-tar",
|
||||
"async-compression",
|
||||
|
|
@ -5386,13 +5471,23 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
|
||||
dependencies = [
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
|
|
@ -5407,6 +5502,16 @@ dependencies = [
|
|||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
|
|
@ -5417,6 +5522,15 @@ dependencies = [
|
|||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
|
|
@ -5469,7 +5583,7 @@ dependencies = [
|
|||
"paste",
|
||||
"profiling",
|
||||
"rand 0.9.5",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.9.0",
|
||||
"simd_helpers",
|
||||
"thiserror 2.0.18",
|
||||
"v_frame",
|
||||
|
|
@ -5799,6 +5913,27 @@ dependencies = [
|
|||
"str_indices",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||
dependencies = [
|
||||
"const-oid 0.9.6",
|
||||
"digest 0.10.7",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha1 0.10.6",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.12.0"
|
||||
|
|
@ -6326,6 +6461,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
|
|
@ -6511,6 +6656,16 @@ dependencies = [
|
|||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.9.0"
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ sysinfo.workspace = true
|
|||
trash.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
rsa = { version = "0.9", features = ["sha1", "pem"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc.workspace = true
|
||||
|
|
|
|||
|
|
@ -316,6 +316,8 @@ async fn start(
|
|||
// Always clean up legacy jar patches and resource packs to ensure clean vanilla state
|
||||
crate::game::skin_server::cleanup_legacy_skin_injection(&cwd, &client_jar).await;
|
||||
|
||||
let mut offline_skin_port = None;
|
||||
|
||||
// Attach proper Authlib-Injector offline skin system if running an offline account
|
||||
if account.is_offline() {
|
||||
let account_uuid_str = account.id.to_string();
|
||||
|
|
@ -325,6 +327,7 @@ async fn start(
|
|||
if let Ok(port) = crate::game::skin_server::ensure_skin_server().await
|
||||
&& let Ok(injector_jar) = crate::game::skin_server::prepare_authlib_injector().await
|
||||
{
|
||||
offline_skin_port = Some(port);
|
||||
let agent_arg = format!(
|
||||
"-javaagent:{}=http://127.0.0.1:{port}",
|
||||
injector_jar.display()
|
||||
|
|
@ -355,6 +358,19 @@ async fn start(
|
|||
profile.resolution.unwrap_or_default(),
|
||||
&java.os_arch,
|
||||
)?;
|
||||
|
||||
if account.is_offline()
|
||||
&& let Some(port) = offline_skin_port
|
||||
&& let Some(user_props) =
|
||||
crate::game::skin_server::build_user_properties(&account.id.to_string(), port).await
|
||||
{
|
||||
for i in 0..mc_args.len() {
|
||||
if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() && mc_args[i + 1] == "{}" {
|
||||
mc_args[i + 1] = user_props;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
arguments::append_profile_game_arguments(&mut mc_args, profile.force_fullscreen, None);
|
||||
|
||||
if let Some(reason) = run_hook(profile.hook_pre.as_deref(), &cwd).await {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ use serde::{Deserialize, Serialize};
|
|||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
use rsa::pkcs8::DecodePrivateKey;
|
||||
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
|
||||
|
||||
type MojangCacheEntry = (Option<String>, Instant);
|
||||
type MojangCacheMap = HashMap<String, MojangCacheEntry>;
|
||||
|
||||
|
|
@ -20,6 +23,26 @@ static SKIN_SERVER_PORT: OnceLock<u16> = OnceLock::new();
|
|||
static REGISTERED_ACCOUNTS: OnceLock<RwLock<HashMap<String, AccountRegistration>>> = OnceLock::new();
|
||||
static MOJANG_CACHE: OnceLock<RwLock<MojangCacheMap>> = OnceLock::new();
|
||||
|
||||
pub static SKIN_SERVER_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ZEWcf6IB84Atm2pnE/N\na19ipdSUpXjMYWaloNhP5ygsMB63i1T55RxaMeVoX/CLY1cFI14+BqgeHGYHOLyo\nVoLz1ylC1QvI99Q1fyl0caTD41K11T5l+EGs5gpepGbQ11tRhtTFNpZg016W83t9\nQMMqpVzSETfK65bu5jOeipvf9TAlXNCpRPZBXe12+kJ5H1t3H9NT2KMUpcYPER7p\nIeIMOgiUwCCBqlXVhoKqjTvplKmyaY/qP3zBdLDQ/+mA1i9SOM8529vi452g+ftP\nkUK+AP52KBb4MnmrwasnH9JdzmS8t2Kl17HYORZ6RpHAPlF4DEC7QDEhvJRLvf1N\nbQIDAQAB\n-----END PUBLIC KEY-----";
|
||||
|
||||
static SKIN_SERVER_PRIVATE_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZkRZx/ogHzgC2\nbamcT81rX2Kl1JSleMxhZqWg2E/nKCwwHreLVPnlHFox5Whf8ItjVwUjXj4GqB4c\nZgc4vKhWgvPXKULVC8j31DV/KXRxpMPjUrXVPmX4QazmCl6kZtDXW1GG1MU2lmDT\nXpbze31AwyqlXNIRN8rrlu7mM56Km9/1MCVc0KlE9kFd7Xb6QnkfW3cf01PYoxSl\nxg8RHukh4gw6CJTAIIGqVdWGgqqNO+mUqbJpj+o/fMF0sND/6YDWL1I4zznb2+Lj\nnaD5+0+RQr4A/nYoFvgyeavBqycf0l3OZLy3YqXXsdg5FnpGkcA+UXgMQLtAMSG8\nlEu9/U1tAgMBAAECggEACCDgDk08ptH++9HYCuN+YLsVE+3/ybjcJe1wVbSPM6sw\nD3IuWFHJ7lHjWsbf4em6Q3FeW0ZrmdMRIO186pU90tGcq5a6jPweO4gdoY0acR5/\nRRBzg66Ln71QaN3NUGYY+lrKjneHkLUIlA0OJbWg5dkE0F3J6WPEvI2MimQ2UaZU\nkqrGKT7QiAhJSlBSAi4dcDXQbSyFRbCm1hw3goL+hpOfFUVymR0TLFqAl8p0uaei\nnGOt4A3g84xx7zHQx4iUVzanX7mGnKDfEHPB1wohvKkn37vGIocAT43kt/AxJS8p\nzMgy1UyyzIwc6vjuDOqLKqTl6dqNvftzucBHTp8NHwKBgQDtUlYCNsyptDwLXHC7\nmIQd4OJIihZxoOwbPx2gzahpaymyeu3/2w6hA2BlqS1/PFpkhe1igOsYkqUWMyNs\nwA9ob4df9CQhPe787q12YL6LahY73A/Qfgn5+6dW8l9pYPyBgwmUd/NMdkQjZILH\nHTxB8eX1F2IsxAKoQ0g8unQ2OwKBgQDqsLiysueH3gL/u19RksQy9PIVi6dnSuiH\nq93zIKVjKcQmAOrQboo9w8wXbwX3z8WGGbxWNbnIEFYTiKzQD2fviserV1d4Xz9E\nOwIDuc8PdBWbgpjSfIDbBqc6AmE34JLyn1AdX9fkhBjquAy370v4ZdHdG5wnpxSg\nw8qQCy7IdwKBgCA5lOo6DLJigeC9DaW7gP0Zo0BcV83YHxdYC6rhIiQmZAQTQywB\nz8u3TKihP0dOp6uMr/43KTUt/HK2QPIsZis1MbmqyhklcsUvl6hCXL1Li3dXW2Jh\nKvOh40ggIyqI++COLYfWfdf9GyV/KW7mHl+J/EK6iR8xAndco3tzigIvAoGAXBRE\nExCwWJVZld59EnND+T4zcRKe9p7kRr6+0TJA0XxEkiiP+IE2Se91Nsh/je/97pRQ\nWX6wynbmXrmkG+m/fLN1jZsyHW85UlrYen+/Zq/D/oSp0wO4RrcAi3j9jb/Vx82L\n0EqXWPgfEpBtpQkFRIsmYNsBVGlwZXcMFaHdlBcCgYAsBi2KNp3/88XdWxODFpEu\nZ5YjqmhGEAInCYTQOquYHFBjVTrV04I2Zr031Z6iETopm/9nLZ15VMjbZyoZWS3B\nAteqz/MfyDOUN3U4xrBr6nxD/HcpNoWeLXyzGnRya8K+/1vz0hWP/e5Q/5KvCGAo\npmSM49/FvJ7y20c1x1fq1A==\n-----END PRIVATE KEY-----";
|
||||
|
||||
static SIGNING_KEY: OnceLock<RsaPrivateKey> = OnceLock::new();
|
||||
|
||||
pub fn sign_sha1_with_rsa(data: &[u8]) -> Option<String> {
|
||||
use sha1::Digest;
|
||||
let key = SIGNING_KEY.get_or_init(|| {
|
||||
RsaPrivateKey::from_pkcs8_pem(SKIN_SERVER_PRIVATE_KEY_PEM)
|
||||
.expect("valid embedded private key")
|
||||
});
|
||||
let mut hasher = sha1::Sha1::new();
|
||||
hasher.update(data);
|
||||
let digest = hasher.finalize();
|
||||
let signing_scheme = Pkcs1v15Sign::new::<sha1::Sha1>();
|
||||
let sig_bytes = key.sign(signing_scheme, &digest).ok()?;
|
||||
Some(base64::engine::general_purpose::STANDARD.encode(sig_bytes))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AccountRegistration {
|
||||
pub uuid: String,
|
||||
|
|
@ -130,7 +153,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
|
|||
let method = parts.next().unwrap_or("");
|
||||
let uri = parts.next().unwrap_or("");
|
||||
|
||||
if method != "GET" && method != "HEAD" {
|
||||
if method != "GET" && method != "HEAD" && method != "POST" {
|
||||
let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
|
||||
stream.write_all(response.as_bytes()).await?;
|
||||
return Ok(());
|
||||
|
|
@ -140,10 +163,15 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
|
|||
|
||||
// 1. Root / Yggdrasil API metadata
|
||||
if path == "/" || path == "/api/yggdrasil" || path == "/api/yggdrasil/" {
|
||||
let body = format!(
|
||||
r#"{{"meta":{{"serverName":"OneLauncher-Cracked","implementationName":"onelauncher-skin-server","implementationVersion":"{}"}},"skinDomains":["127.0.0.1","localhost"]}}"#,
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
let body = serde_json::json!({
|
||||
"meta": {
|
||||
"serverName": "OneLauncher-Cracked",
|
||||
"implementationName": "onelauncher-skin-server",
|
||||
"implementationVersion": env!("CARGO_PKG_VERSION")
|
||||
},
|
||||
"skinDomains": ["127.0.0.1", "localhost"],
|
||||
"signaturePublickey": SKIN_SERVER_PUBLIC_KEY
|
||||
}).to_string();
|
||||
let header = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
|
|
@ -153,8 +181,52 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// 2. Profile request: /session/minecraft/profile/<uuid>
|
||||
if let Some(uuid_raw) = path.strip_prefix("/session/minecraft/profile/") {
|
||||
// 2. Public keys endpoint (for Minecraft 1.19+ profile signature verification)
|
||||
if path == "/publickeys" || path == "/minecraftservices/publickeys" {
|
||||
let key_b64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ZEWcf6IB84Atm2pnE/Na19ipdSUpXjMYWaloNhP5ygsMB63i1T55RxaMeVoX/CLY1cFI14+BqgeHGYHOLyoVoLz1ylC1QvI99Q1fyl0caTD41K11T5l+EGs5gpepGbQ11tRhtTFNpZg016W83t9QMMqpVzSETfK65bu5jOeipvf9TAlXNCpRPZBXe12+kJ5H1t3H9NT2KMUpcYPER7pIeIMOgiUwCCBqlXVhoKqjTvplKmyaY/qP3zBdLDQ/+mA1i9SOM8529vi452g+ftPkUK+AP52KBb4MnmrwasnH9JdzmS8t2Kl17HYORZ6RpHAPlF4DEC7QDEhvJRLvf1NbQIDAQAB";
|
||||
let body = serde_json::json!({
|
||||
"profilePropertyKeys": [{"publicKey": key_b64}],
|
||||
"playerCertificateKeys": [{"publicKey": key_b64}]
|
||||
}).to_string();
|
||||
let header = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream.write_all(header.as_bytes()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. Profiles by names endpoint (bulk profile lookup)
|
||||
if path == "/api/profiles/minecraft" || path == "/api/profiles/minecraft/" {
|
||||
let mut profiles = Vec::new();
|
||||
if let Some(accounts) = REGISTERED_ACCOUNTS.get() {
|
||||
let read = accounts.read();
|
||||
for reg in read.values() {
|
||||
if !profiles.iter().any(|p: &serde_json::Value| p["id"] == reg.undashed_uuid) {
|
||||
profiles.push(serde_json::json!({
|
||||
"id": reg.undashed_uuid,
|
||||
"name": reg.username,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
let body = serde_json::to_string(&profiles).unwrap_or_else(|_| "[]".to_string());
|
||||
let header = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream.write_all(header.as_bytes()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 4. Profile request: /sessionserver/session/minecraft/profile/<uuid> or /session/minecraft/profile/<uuid>
|
||||
let profile_uuid = path
|
||||
.strip_prefix("/sessionserver/session/minecraft/profile/")
|
||||
.or_else(|| path.strip_prefix("/session/minecraft/profile/"));
|
||||
|
||||
if let Some(uuid_raw) = profile_uuid {
|
||||
let uuid_str = uuid_raw.trim_matches('/');
|
||||
|
||||
// Check if this is one of our local accounts with a custom skin
|
||||
|
|
@ -185,7 +257,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. Texture request: /textures/<id>.png
|
||||
// 5. Texture request: /textures/<id>.png
|
||||
if let Some(texture_raw) = path.strip_prefix("/textures/") {
|
||||
let texture_id = texture_raw.trim_matches('/').trim_end_matches(".png");
|
||||
if let Some(png_bytes) = load_skin_png(texture_id).await {
|
||||
|
|
@ -328,13 +400,71 @@ async fn build_profile_response(uuid_input: &str, port: u16) -> Option<String> {
|
|||
let encoded_textures =
|
||||
base64::engine::general_purpose::STANDARD.encode(texture_payload.as_bytes());
|
||||
|
||||
let profile_json = format!(
|
||||
r#"{{"id":"{undashed}","name":"{username}","properties":[{{"name":"textures","value":"{encoded_textures}"}}]}}"#
|
||||
);
|
||||
let profile_json = if let Some(sig) = sign_sha1_with_rsa(encoded_textures.as_bytes()) {
|
||||
format!(
|
||||
r#"{{"id":"{undashed}","name":"{username}","properties":[{{"name":"textures","value":"{encoded_textures}","signature":"{sig}"}}]}}"#
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"{{"id":"{undashed}","name":"{username}","properties":[{{"name":"textures","value":"{encoded_textures}"}}]}}"#
|
||||
)
|
||||
};
|
||||
|
||||
Some(profile_json)
|
||||
}
|
||||
|
||||
/// Builds the `--userProperties` JSON map for launch arguments with signed textures.
|
||||
pub async fn build_user_properties(uuid_input: &str, port: u16) -> Option<String> {
|
||||
let dashed = to_dashed_uuid(uuid_input);
|
||||
let undashed = to_undashed_uuid(uuid_input);
|
||||
|
||||
let _skin_path = find_skin_path(uuid_input).await?;
|
||||
|
||||
let is_slim = if let Ok(meta_p) = skin_meta_path(&dashed) {
|
||||
if meta_p.exists() {
|
||||
polyio::read_to_string(&meta_p)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<SkinMetadata>(&s).ok())
|
||||
.map(|m| m.is_slim)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let username = resolve_username(uuid_input).await;
|
||||
let skin_url = format!("http://127.0.0.1:{port}/textures/{undashed}.png");
|
||||
|
||||
let model_json = if is_slim {
|
||||
r#","metadata":{"model":"slim"}"#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let timestamp = chrono::Utc::now().timestamp_millis();
|
||||
let texture_payload = format!(
|
||||
r#"{{"timestamp":{timestamp},"profileId":"{undashed}","profileName":"{username}","textures":{{"SKIN":{{"url":"{skin_url}"{model_json}}}}}}}"#
|
||||
);
|
||||
|
||||
let encoded_textures =
|
||||
base64::engine::general_purpose::STANDARD.encode(texture_payload.as_bytes());
|
||||
|
||||
let prop_json = if let Some(sig) = sign_sha1_with_rsa(encoded_textures.as_bytes()) {
|
||||
format!(
|
||||
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}","signature":"{sig}"}}]}}"#
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}"}}]}}"#
|
||||
)
|
||||
};
|
||||
|
||||
Some(prop_json)
|
||||
}
|
||||
|
||||
async fn fetch_mojang_profile(uuid_str: &str) -> Option<String> {
|
||||
let undashed = to_undashed_uuid(uuid_str);
|
||||
if undashed.len() != 32 {
|
||||
|
|
@ -360,7 +490,10 @@ async fn fetch_mojang_profile(uuid_str: &str) -> Option<String> {
|
|||
"https://sessionserver.mojang.com/session/minecraft/profile/{undashed}?unsigned=false"
|
||||
);
|
||||
let result = match client.get(&url).send().await {
|
||||
Ok(res) if res.status().is_success() => res.text().await.ok(),
|
||||
Ok(res) if res.status() == reqwest::StatusCode::OK => match res.text().await {
|
||||
Ok(text) if !text.trim().is_empty() => Some(text),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
|
@ -565,4 +698,87 @@ mod tests {
|
|||
let meta = tokio::fs::metadata(&jar_path).await.unwrap();
|
||||
assert!(meta.len() > 100_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_sha1_with_rsa() {
|
||||
let sig = sign_sha1_with_rsa(b"hello world test");
|
||||
assert!(sig.is_some());
|
||||
let sig_str = sig.unwrap();
|
||||
assert!(!sig_str.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_skin_server_http_endpoints() {
|
||||
let port = ensure_skin_server().await.expect("skin server starts");
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// 1. Test GET /
|
||||
let res = client
|
||||
.get(format!("http://127.0.0.1:{port}/"))
|
||||
.send()
|
||||
.await
|
||||
.expect("GET / succeeds");
|
||||
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||
let root_json: serde_json::Value = res.json().await.expect("valid JSON");
|
||||
assert!(root_json.get("signaturePublickey").is_some());
|
||||
assert!(root_json.get("skinDomains").is_some());
|
||||
|
||||
// 2. Test GET /publickeys
|
||||
let res = client
|
||||
.get(format!("http://127.0.0.1:{port}/publickeys"))
|
||||
.send()
|
||||
.await
|
||||
.expect("GET /publickeys succeeds");
|
||||
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||
let pk_json: serde_json::Value = res.json().await.expect("valid JSON");
|
||||
assert!(pk_json.get("profilePropertyKeys").is_some());
|
||||
|
||||
// 3. Test GET /sessionserver/session/minecraft/profile/<unknown_uuid>
|
||||
// Unknown player must return 204 No Content so other multiplayer players never get user skin!
|
||||
let res = client
|
||||
.get(format!("http://127.0.0.1:{port}/sessionserver/session/minecraft/profile/00000000000000000000000000000000"))
|
||||
.send()
|
||||
.await
|
||||
.expect("GET profile succeeds");
|
||||
assert_eq!(res.status(), reqwest::StatusCode::NO_CONTENT);
|
||||
|
||||
// 4. Test registered user with a skin
|
||||
let test_uuid = "12345678-1234-1234-1234-123456789abc";
|
||||
let test_undashed = "12345678123412341234123456789abc";
|
||||
let test_user = "TestPlayer";
|
||||
register_launch_account(test_uuid, test_user);
|
||||
|
||||
if let Ok(dir) = skins_dir() {
|
||||
let _ = polyio::create_dir_all(&dir).await;
|
||||
let skin_p = dir.join(format!("{test_uuid}.png"));
|
||||
// 1x1 png or dummy png
|
||||
let dummy_png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15c4\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82";
|
||||
let _ = polyio::write(&skin_p, dummy_png).await;
|
||||
|
||||
let res = client
|
||||
.get(format!("http://127.0.0.1:{port}/sessionserver/session/minecraft/profile/{test_undashed}"))
|
||||
.send()
|
||||
.await
|
||||
.expect("GET registered profile succeeds");
|
||||
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||
let prof: serde_json::Value = res.json().await.expect("valid profile JSON");
|
||||
assert_eq!(prof["name"], test_user);
|
||||
let props = prof["properties"].as_array().expect("properties array");
|
||||
assert!(!props.is_empty());
|
||||
assert_eq!(props[0]["name"], "textures");
|
||||
assert!(props[0]["value"].as_str().is_some());
|
||||
assert!(props[0]["signature"].as_str().is_some());
|
||||
|
||||
// Test GET /textures/<undashed>.png
|
||||
let res = client
|
||||
.get(format!("http://127.0.0.1:{port}/textures/{test_undashed}.png"))
|
||||
.send()
|
||||
.await
|
||||
.expect("GET texture succeeds");
|
||||
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||
assert_eq!(res.headers()["content-type"], "image/png");
|
||||
|
||||
let _ = polyio::remove_file(&skin_p).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue