diff --git a/README.en.md b/README.en.md index 3b0ae5b..361eda4 100644 --- a/README.en.md +++ b/README.en.md @@ -103,9 +103,6 @@ Configure Minecraft server with the following JVM parameter: If this this feature is enabled, Minecraft will send a POST request to /minecraftservices/player/certificates to retrieve the key pair issued by the authentication server. It's disabled by default if the authentication server does NOT send feature.enable_profile_key option. - If the profile signing key isn't present, the player will be unable to join servers that enable enforce-secure-profile=true option. - And other players' Minecraft client will log a warning message when receiving an unsigned chat message. - -Dauthlibinjector.usernameCheck={default|enabled|disabled} Whether to enable username validation. If disabled, Minecraft, BungeeCord and Paper will NOT perform username validation. It's disabled by default if the authentication server does NOT send feature.usernameCheck option. diff --git a/README.md b/README.md index 6334003..cc541d2 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,6 @@ gradle 启用此功能后, Minecraft 会向 /minecraftservices/player/certificates 发送 POST 请求, 以获取由验证服务器颁发的密钥对. 此功能需要验证服务器支持, 若验证服务器未设置 feature.enable_profile_key 选项, 则该功能默认禁用. - 当缺少消息签名密钥时, 玩家将无法进入设置了 enforce-secure-profile=true 选项的服务器. - 而当其他玩家的客户端在收到无有效签名的聊天消息时, 会在日志中记录警告. - -Dauthlibinjector.usernameCheck={default|enabled|disabled} 是否启用玩家用户名检查, 若禁用, 则 authlib-injector 将关闭 Minecraft、BungeeCord 和 Paper 的用户名检查功能. 若验证服务器未设置 feature.usernameCheck 选项, 则默认禁用. diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java index 02e1cf2..d0e5a53 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java @@ -16,11 +16,21 @@ */ package moe.yushi.authlibinjector.httpd; +import static java.nio.charset.StandardCharsets.UTF_8; +import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON; import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Base64; import java.util.Optional; import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response; import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status; +import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject; /** * Intercepts Minecraft's request to https://api.minecraftservices.com/player/certificates, @@ -36,9 +46,39 @@ public class ProfileKeyFilter implements URLFilter { @Override public Optional handle(String domain, String path, IHTTPSession session) throws IOException { if (domain.equals("api.minecraftservices.com") && path.equals("/player/certificates") && session.getMethod().equals("POST")) { - return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, makeDummyResponse().toJSONString())); } return Optional.empty(); } + private JSONObject makeDummyResponse() { + KeyPairGenerator generator; + try { + generator = KeyPairGenerator.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + Base64.Encoder base64 = Base64.getMimeEncoder(76, "\n".getBytes(UTF_8)); + String publicKeyPEM = "-----BEGIN RSA PUBLIC KEY-----\n" + base64.encodeToString(keyPair.getPublic().getEncoded()) + "\n-----END RSA PUBLIC KEY-----\n"; + String privateKeyPEM = "-----BEGIN RSA PRIVATE KEY-----\n" + base64.encodeToString(keyPair.getPrivate().getEncoded()) + "\n-----END RSA PRIVATE KEY-----\n"; + + Instant now = Instant.now(); + Instant expiresAt = now.plus(48, ChronoUnit.HOURS); + Instant refreshedAfter = now.plus(36, ChronoUnit.HOURS); + + JSONObject response = new JSONObject(); + JSONObject keyPairObj = new JSONObject(); + keyPairObj.put("privateKey", privateKeyPEM); + keyPairObj.put("publicKey", publicKeyPEM); + response.put("keyPair", keyPairObj); + response.put("publicKeySignature", "AA=="); + response.put("publicKeySignatureV2", "AA=="); + response.put("expiresAt", DateTimeFormatter.ISO_INSTANT.format(expiresAt)); + response.put("refreshedAfter", DateTimeFormatter.ISO_INSTANT.format(refreshedAfter)); + return response; + } + } diff --git a/src/main/java/moe/yushi/authlibinjector/transform/support/YggdrasilKeyTransformUnit.java b/src/main/java/moe/yushi/authlibinjector/transform/support/YggdrasilKeyTransformUnit.java index 8889f9c..2cb1cc1 100644 --- a/src/main/java/moe/yushi/authlibinjector/transform/support/YggdrasilKeyTransformUnit.java +++ b/src/main/java/moe/yushi/authlibinjector/transform/support/YggdrasilKeyTransformUnit.java @@ -19,6 +19,7 @@ package moe.yushi.authlibinjector.transform.support; import static moe.yushi.authlibinjector.util.IOUtils.asBytes; import static moe.yushi.authlibinjector.util.Logging.Level.DEBUG; import static org.objectweb.asm.Opcodes.ALOAD; +import static org.objectweb.asm.Opcodes.ARETURN; import static org.objectweb.asm.Opcodes.ASM9; import static org.objectweb.asm.Opcodes.GETFIELD; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; @@ -26,6 +27,8 @@ import static org.objectweb.asm.Opcodes.IRETURN; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.util.Base64; @@ -78,6 +81,56 @@ public class YggdrasilKeyTransformUnit implements TransformUnit { return false; } + @CallbackMethod + public static Signature createDummySignature() { + Signature sig = new Signature("authlib-injector-dummy-verify") { + + @Override + protected boolean engineVerify(byte[] sigBytes) { + return true; + } + + @Override + protected void engineUpdate(byte[] b, int off, int len) { + + } + + @Override + protected void engineUpdate(byte b) { + } + + @Override + protected byte[] engineSign() { + throw new UnsupportedOperationException(); + } + + @Override + protected void engineSetParameter(String param, Object value) { + + } + + @Override + protected void engineInitVerify(PublicKey publicKey) { + } + + @Override + protected void engineInitSign(PrivateKey privateKey) { + throw new UnsupportedOperationException(); + } + + @Override + protected Object engineGetParameter(String param) { + return null; + } + }; + try { + sig.initVerify((PublicKey) null); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + return sig; + } + @Override public Optional transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) { if ("com.mojang.authlib.properties.Property".equals(className)) { @@ -124,6 +177,19 @@ public class YggdrasilKeyTransformUnit implements TransformUnit { mv.visitEnd(); return null; + + } else if ("signature".equals(name) && "()Ljava/security/Signature;".equals(desc)) { + ctx.markModified(); + + MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); + mv.visitCode(); + ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "createDummySignature"); + mv.visitInsn(ARETURN); + mv.visitMaxs(-1, -1); + mv.visitEnd(); + + return null; + } else { return super.visitMethod(access, name, desc, signature, exceptions); }