Skip to content

Commit

Permalink
Merge pull request #172 from yushijinhun/develop
Browse files Browse the repository at this point in the history
Release v1.2.0
  • Loading branch information
yushijinhun authored Aug 4, 2022
2 parents 8dde9cb + c888217 commit 7e05168
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 51 deletions.
3 changes: 0 additions & 3 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 选项, 则默认禁用.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,9 +46,39 @@ public boolean canHandle(String domain) {
@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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,41 +40,31 @@
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;

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;
}
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");
Expand All @@ -80,37 +73,91 @@ 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);
}
}

Logging.log(Level.WARNING, "Failed to verify property signature");
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
Expand All @@ -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);
}
Expand Down
Binary file added src/main/resources/mojang_publickey.der
Binary file not shown.

0 comments on commit 7e05168

Please sign in to comment.