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 boolean canHandle(String domain) { @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 86b376b..2cb1cc1 100644 --- a/src/main/java/moe/yushi/authlibinjector/transform/support/YggdrasilKeyTransformUnit.java +++ b/src/main/java/moe/yushi/authlibinjector/transform/support/YggdrasilKeyTransformUnit.java @@ -16,18 +16,21 @@ */ package moe.yushi.authlibinjector.transform.support; -import static java.lang.invoke.MethodHandles.publicLookup; -import static java.lang.invoke.MethodType.methodType; +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; import static org.objectweb.asm.Opcodes.IRETURN; -import java.lang.invoke.MethodHandle; +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.security.SignatureException; import java.util.Base64; import java.util.List; import java.util.Optional; @@ -37,6 +40,7 @@ import moe.yushi.authlibinjector.transform.CallbackMethod; import moe.yushi.authlibinjector.transform.TransformContext; import moe.yushi.authlibinjector.transform.TransformUnit; +import moe.yushi.authlibinjector.util.KeyUtils; import moe.yushi.authlibinjector.util.Logging; import moe.yushi.authlibinjector.util.Logging.Level; @@ -44,34 +48,23 @@ public class YggdrasilKeyTransformUnit implements TransformUnit { public static final List PUBLIC_KEYS = new CopyOnWriteArrayList<>(); - @CallbackMethod - public static boolean verifyPropertySignature(Object property, PublicKey mojangKey) throws Throwable { - MethodHandle verifyAction = publicLookup().bind(property, "isSignatureValid", methodType(boolean.class, PublicKey.class)); + static { + PUBLIC_KEYS.add(loadMojangPublicKey()); + } - if ((boolean) verifyAction.invokeExact(mojangKey)) { - return true; - } - for (PublicKey customKey : PUBLIC_KEYS) { - if ((boolean) verifyAction.invokeExact(customKey)) { - return true; - } + private static PublicKey loadMojangPublicKey() { + try (InputStream in = YggdrasilKeyTransformUnit.class.getResourceAsStream("/mojang_publickey.der")) { + return KeyUtils.parseX509PublicKey(asBytes(in)); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to load Mojang public key", e); } - return false; } @CallbackMethod - public static boolean verifyPropertySignatureNew(Signature mojangSignatureObj, String propertyValue, String base64Signature) { + public static boolean verifyPropertySignature(String propertyValue, String base64Signature) { byte[] sig = Base64.getDecoder().decode(base64Signature); byte[] data = propertyValue.getBytes(); - try { - mojangSignatureObj.update(data); - if (mojangSignatureObj.verify(sig)) - return true; - } catch (SignatureException e) { - Logging.log(DEBUG, "Failed to verify signature with Mojang's key", e); - } - for (PublicKey customKey : PUBLIC_KEYS) { try { Signature signature = Signature.getInstance("SHA1withRSA"); @@ -80,7 +73,7 @@ public static boolean verifyPropertySignatureNew(Signature mojangSignatureObj, S if (signature.verify(sig)) return true; } catch (GeneralSecurityException e) { - Logging.log(DEBUG, "Failed to verify signature with custom key " + customKey, e); + Logging.log(DEBUG, "Failed to verify signature with key " + customKey, e); } } @@ -88,29 +81,83 @@ public static boolean verifyPropertySignatureNew(Signature mojangSignatureObj, S 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.yggdrasil.YggdrasilMinecraftSessionService".equals(className)) { + if ("com.mojang.authlib.properties.Property".equals(className)) { return Optional.of(new ClassVisitor(ASM9, writer) { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - return new MethodVisitor(ASM9, super.visitMethod(access, name, desc, signature, exceptions)) { - @Override - public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - if (opcode == INVOKEVIRTUAL - && "com/mojang/authlib/properties/Property".equals(owner) - && "isSignatureValid".equals(name) - && "(Ljava/security/PublicKey;)Z".equals(descriptor)) { - ctx.markModified(); - ctx.invokeCallback(this, YggdrasilKeyTransformUnit.class, "verifyPropertySignature"); - } else { - super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - } - } - }; - } + if ("isSignatureValid".equals(name) && "(Ljava/security/PublicKey;)Z".equals(desc)) { + ctx.markModified(); + + MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, "com/mojang/authlib/properties/Property", "value", "Ljava/lang/String;"); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, "com/mojang/authlib/properties/Property", "signature", "Ljava/lang/String;"); + ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "verifyPropertySignature"); + mv.visitInsn(IRETURN); + mv.visitMaxs(-1, -1); + mv.visitEnd(); + return null; + } else { + return super.visitMethod(access, name, desc, signature, exceptions); + } + } }); + } else if ("com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo".equals(className)) { return Optional.of(new ClassVisitor(ASM9, writer) { @Override @@ -120,18 +167,29 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); mv.visitCode(); - mv.visitVarInsn(ALOAD, 0); - mv.visitMethodInsn(INVOKEVIRTUAL, "com/mojang/authlib/yggdrasil/YggdrasilServicesKeyInfo", "signature", "()Ljava/security/Signature;", false); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEVIRTUAL, "com/mojang/authlib/properties/Property", "getValue", "()Ljava/lang/String;", false); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEVIRTUAL, "com/mojang/authlib/properties/Property", "getSignature", "()Ljava/lang/String;", false); - ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "verifyPropertySignatureNew"); + ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "verifyPropertySignature"); mv.visitInsn(IRETURN); mv.visitMaxs(-1, -1); 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); } diff --git a/src/main/resources/mojang_publickey.der b/src/main/resources/mojang_publickey.der new file mode 100644 index 0000000..9c79a3a Binary files /dev/null and b/src/main/resources/mojang_publickey.der differ