feat: 1.20 port
This commit is contained in:
11
common/build.gradle
Normal file
11
common/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
architectury {
|
||||
common rootProject.enabled_platforms.split(',')
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// We depend on Fabric Loader here to use the Fabric @Environment annotations,
|
||||
// which get remapped to the correct annotations on each platform.
|
||||
// Do NOT use other classes from Fabric Loader.
|
||||
modImplementation "net.fabricmc:fabric-loader:$rootProject.fabric_loader_version"
|
||||
compileOnly("com.mojang:authlib:4.0.43")
|
||||
}
|
||||
17
common/src/main/java/net/magicterra/skinfix/SkinFixMod.java
Normal file
17
common/src/main/java/net/magicterra/skinfix/SkinFixMod.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package net.magicterra.skinfix;
|
||||
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
public final class SkinFixMod {
|
||||
public static final String MOD_ID = "skinfix";
|
||||
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
public static final String TEXTURE_DOMAIN_SUFFIX = ".gardel.top";
|
||||
|
||||
public static void init() {
|
||||
// Write common init code here.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.magicterra.skinfix.inject;
|
||||
|
||||
import java.net.IDN;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import net.magicterra.skinfix.SkinFixMod;
|
||||
|
||||
public class InjectTextureUrlChecker {
|
||||
private static final Set<String> ALLOWED_SCHEMES = Set.of(
|
||||
"http",
|
||||
"https"
|
||||
);
|
||||
|
||||
private static final List<String> ALLOWED_DOMAINS = List.of(
|
||||
".minecraft.net",
|
||||
".mojang.com",
|
||||
SkinFixMod.TEXTURE_DOMAIN_SUFFIX
|
||||
);
|
||||
|
||||
private static final List<String> BLOCKED_DOMAINS = List.of(
|
||||
"bugs.mojang.com",
|
||||
"education.minecraft.net",
|
||||
"feedback.minecraft.net"
|
||||
);
|
||||
|
||||
public static boolean isAllowedTextureDomain(final String url) {
|
||||
final URI uri;
|
||||
|
||||
try {
|
||||
uri = new URI(url).normalize();
|
||||
} catch (final URISyntaxException ignored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String scheme = uri.getScheme();
|
||||
if (scheme == null || !ALLOWED_SCHEMES.contains(scheme)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String domain = uri.getHost();
|
||||
if (domain == null) {
|
||||
return false;
|
||||
}
|
||||
final String decodedDomain = IDN.toUnicode(domain);
|
||||
final String lowerCaseDomain = decodedDomain.toLowerCase(Locale.ROOT);
|
||||
if (!lowerCaseDomain.equals(decodedDomain)) {
|
||||
return false;
|
||||
}
|
||||
return isDomainOnList(decodedDomain, ALLOWED_DOMAINS) && !isDomainOnList(decodedDomain, BLOCKED_DOMAINS);
|
||||
}
|
||||
|
||||
private static boolean isDomainOnList(final String domain, final List<String> list) {
|
||||
for (final String entry : list) {
|
||||
if (domain.endsWith(entry)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package net.magicterra.skinfix.inject;
|
||||
|
||||
import com.mojang.authlib.Environment;
|
||||
import com.mojang.authlib.EnvironmentParser;
|
||||
import com.mojang.authlib.GameProfileRepository;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||
import com.mojang.authlib.minecraft.UserApiService;
|
||||
import com.mojang.authlib.yggdrasil.ServicesKeySet;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilEnvironment;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilGameProfileRepository;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilUserApiService;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* InjectYggdrasilAuthenticationService
|
||||
*
|
||||
* @author Gardel <gardel741@outlook.com>
|
||||
* @since 2025-04-26 22:10
|
||||
*/
|
||||
public class InjectYggdrasilAuthenticationService extends YggdrasilAuthenticationService {
|
||||
private final ServicesKeySet servicesKeySet;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public InjectYggdrasilAuthenticationService(Proxy proxy) {
|
||||
this(proxy, EnvironmentParser
|
||||
.getEnvironmentFromProperties()
|
||||
.orElse(YggdrasilEnvironment.PROD.getEnvironment()));
|
||||
}
|
||||
|
||||
public InjectYggdrasilAuthenticationService(Proxy proxy, Environment environment) {
|
||||
super(proxy, environment);
|
||||
this.environment = environment;
|
||||
|
||||
final URL publicKeySetUrl = HttpAuthenticationService.constantURL("https://mc.gardel.top/minecraftservices/publickeys");
|
||||
this.servicesKeySet = YggdrasilServicesKeyInfo.get(publicKeySetUrl, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinecraftSessionService createMinecraftSessionService() {
|
||||
return new InjectYggdrasilMinecraftSessionService(this, environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServicesKeySet getServicesKeySet() {
|
||||
return servicesKeySet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfileRepository createProfileRepository() {
|
||||
return new YggdrasilGameProfileRepository(this, Environment.create(
|
||||
environment.getAuthHost(),
|
||||
"https://mc.gardel.top/api",
|
||||
environment.getSessionHost(),
|
||||
environment.getServicesHost(),
|
||||
environment.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserApiService createUserApiService(String accessToken) throws AuthenticationException {
|
||||
return new YggdrasilUserApiService(accessToken, getProxy(), Environment.create(
|
||||
environment.getAuthHost(),
|
||||
environment.getAccountsHost(),
|
||||
environment.getSessionHost(),
|
||||
"https://mc.gardel.top/minecraftservices",
|
||||
environment.getName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.magicterra.skinfix.inject;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.mojang.authlib.Environment;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.minecraft.InsecurePublicKeyException;
|
||||
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.yggdrasil.ServicesKeySet;
|
||||
import com.mojang.authlib.yggdrasil.ServicesKeyType;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
|
||||
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* InjectYggdrasilMinecraftSessionService
|
||||
*
|
||||
* @author Gardel <gardel741@outlook.com>
|
||||
* @since 2025-04-26 22:11
|
||||
*/
|
||||
public class InjectYggdrasilMinecraftSessionService extends YggdrasilMinecraftSessionService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(InjectYggdrasilMinecraftSessionService.class);
|
||||
|
||||
private final Gson gson;
|
||||
|
||||
protected InjectYggdrasilMinecraftSessionService(YggdrasilAuthenticationService service, Environment env) {
|
||||
super(service, env);
|
||||
try {
|
||||
Field field = YggdrasilMinecraftSessionService.class.getDeclaredField("gson");
|
||||
field.setAccessible(true);
|
||||
gson = (Gson) field.get(this);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure) throws InsecurePublicKeyException {
|
||||
final Property textureProperty = Iterables.getFirst(profile.getProperties().get("textures"), null);
|
||||
|
||||
if (textureProperty == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
final String value = requireSecure ? getSecurePropertyValue(textureProperty) : textureProperty.getValue();
|
||||
|
||||
final MinecraftTexturesPayload result;
|
||||
try {
|
||||
final String json = new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
|
||||
result = gson.fromJson(json, MinecraftTexturesPayload.class);
|
||||
} catch (final JsonParseException e) {
|
||||
LOGGER.error("Could not decode textures payload", e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
if (result == null || result.getTextures() == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
for (final Map.Entry<MinecraftProfileTexture.Type, MinecraftProfileTexture> entry : result.getTextures().entrySet()) {
|
||||
final String url = entry.getValue().getUrl();
|
||||
if (!InjectTextureUrlChecker.isAllowedTextureDomain(url)) {
|
||||
LOGGER.error("Textures payload contains blocked domain: {}", url);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
return result.getTextures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecurePropertyValue(final Property property) throws InsecurePublicKeyException {
|
||||
if (!property.hasSignature()) {
|
||||
LOGGER.error("Signature is missing from Property {}", property.getName());
|
||||
throw new InsecurePublicKeyException.MissingException();
|
||||
}
|
||||
|
||||
final ServicesKeySet servicesKeySet = getAuthenticationService().getServicesKeySet();
|
||||
if (servicesKeySet.keys(ServicesKeyType.PROFILE_PROPERTY).stream().noneMatch(key -> key.validateProperty(property))) {
|
||||
LOGGER.error("Property {} has been tampered with (signature invalid)", property.getName());
|
||||
throw new InsecurePublicKeyException.InvalidException("Property has been tampered with (signature invalid)");
|
||||
}
|
||||
|
||||
return property.getValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.magicterra.skinfix.mixin;
|
||||
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import java.net.Proxy;
|
||||
import net.magicterra.skinfix.inject.InjectYggdrasilAuthenticationService;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.slf4j.Logger;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
/**
|
||||
* MinecraftMixin
|
||||
*
|
||||
* @author Gardel <gardel741@outlook.com>
|
||||
* @since 2025-04-26 19:03
|
||||
*/
|
||||
@Mixin(Minecraft.class)
|
||||
public abstract class MinecraftMixin {
|
||||
@Final
|
||||
@Shadow
|
||||
private static Logger LOGGER;
|
||||
|
||||
@Redirect(
|
||||
method = "<init>",
|
||||
at = @At(value = "NEW", target = "(Ljava/net/Proxy;)Lcom/mojang/authlib/yggdrasil/YggdrasilAuthenticationService;", remap = false)
|
||||
)
|
||||
public YggdrasilAuthenticationService getCustomYggdrasilAuthenticationService(Proxy proxy) {
|
||||
LOGGER.info("injecting custom Yggdrasil authentication service");
|
||||
return new InjectYggdrasilAuthenticationService(proxy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.magicterra.skinfix.mixin;
|
||||
|
||||
import java.util.UUID;
|
||||
import net.minecraft.util.SignatureValidator;
|
||||
import net.minecraft.world.entity.player.ProfilePublicKey;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
|
||||
/**
|
||||
* 跳过 ProfileKey 校验
|
||||
*
|
||||
* @author Gardel <gardel741@outlook.com>
|
||||
* @since 2025-06-06 02:54
|
||||
*/
|
||||
@Mixin(ProfilePublicKey.Data.class)
|
||||
public abstract class ProfilePublicKeyDataMixin {
|
||||
/**
|
||||
* @author Gardel
|
||||
* @reason skip verify
|
||||
*/
|
||||
@Overwrite
|
||||
boolean validateSignature(SignatureValidator signatureValidator, UUID profileId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
BIN
common/src/main/resources/assets/skinfix/icon.png
Normal file
BIN
common/src/main/resources/assets/skinfix/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 922 B |
15
common/src/main/resources/skinfix.mixins.json
Normal file
15
common/src/main/resources/skinfix.mixins.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "net.magicterra.skinfix.mixin",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"minVersion": "0.8",
|
||||
"client": [
|
||||
"MinecraftMixin"
|
||||
],
|
||||
"mixins": [
|
||||
"ProfilePublicKeyDataMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user