feat: 1.20 port

This commit is contained in:
2025-09-01 01:43:22 +08:00
commit 62eee93fea
27 changed files with 1137 additions and 0 deletions

11
common/build.gradle Normal file
View 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")
}

View 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.
}
}

View File

@@ -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;
}
}

View File

@@ -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 &lt;gardel741@outlook.com&gt;
* @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()));
}
}

View File

@@ -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 &lt;gardel741@outlook.com&gt;
* @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();
}
}

View File

@@ -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 &lt;gardel741@outlook.com&gt;
* @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);
}
}

View File

@@ -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 &lt;gardel741@outlook.com&gt;
* @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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

View 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
}
}