forked from MirrorHub/authlib-injector
commit
7e051682b7
|
@ -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.
|
||||
|
|
|
@ -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 选项, 则默认禁用.
|
||||
|
|
|
@ -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<Response> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 org.objectweb.asm.MethodVisitor;
|
|||
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<PublicKey> 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;
|
||||
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);
|
||||
}
|
||||
for (PublicKey customKey : PUBLIC_KEYS) {
|
||||
if ((boolean) verifyAction.invokeExact(customKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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 class YggdrasilKeyTransformUnit implements TransformUnit {
|
|||
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 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<ClassVisitor> 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 class YggdrasilKeyTransformUnit implements TransformUnit {
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
BIN
src/main/resources/mojang_publickey.der
Normal file
BIN
src/main/resources/mojang_publickey.der
Normal file
Binary file not shown.
Loading…
Reference in a new issue