From 3a4b4e129f0d5185eadd98b176f3bcfb6befb164 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Wed, 22 Sep 2021 13:38:26 +0800 Subject: [PATCH] fix: offline account skin --- .../hmcl/auth/offline/OfflineAccount.java | 5 +- .../hmcl/auth/offline/YggdrasilServer.java | 103 ++++++++++++------ .../org/jackhuang/hmcl/util/KeyUtils.java | 43 ++++++++ .../jackhuang/hmcl/util/io/HttpServer.java | 2 +- 4 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyUtils.java diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index a223875dc..76f7553b5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -23,6 +23,7 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException; +import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; import org.jackhuang.hmcl.auth.yggdrasil.TextureType; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.util.StringUtils; @@ -110,14 +111,14 @@ public class OfflineAccount extends Account { try { YggdrasilServer server = new YggdrasilServer(0); server.start(); - server.addCharacter(new YggdrasilServer.Character(uuid, username, YggdrasilServer.ModelType.STEVE, + server.addCharacter(new YggdrasilServer.Character(uuid, username, TextureModel.STEVE, mapOf( pair(TextureType.SKIN, server.loadTexture(skin)), pair(TextureType.CAPE, server.loadTexture(cape)) ))); return authInfo.withArguments(new Arguments().addJVMArguments( - "-javaagent:" + artifact.getLocation().toString() + "=" + "http://127.0.0.1:" + server.getListeningPort(), + "-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(), "-Dauthlibinjector.side=client" )) .withCloseable(server::stop); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index be807f6fb..9f839dbdf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -18,9 +18,10 @@ package org.jackhuang.hmcl.auth.offline; import com.google.gson.reflect.TypeToken; -import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; +import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; import org.jackhuang.hmcl.auth.yggdrasil.TextureType; +import org.jackhuang.hmcl.util.KeyUtils; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; @@ -36,13 +37,13 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.security.*; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; @@ -67,7 +68,11 @@ public class YggdrasilServer extends HttpServer { private Response root(Request request) { return ok(mapOf( - pair("skinDomains", Collections.emptyList()), + pair("signaturePublickey", KeyUtils.toPEMPublicKey(getSignaturePublicKey())), + pair("skinDomains", Arrays.asList( + "127.0.0.1", + "localhost" + )), pair("meta", mapOf( pair("serverName", "HMCL Offline Account Skin/Cape Server"), pair("implementationName", "HMCL"), @@ -86,7 +91,7 @@ public class YggdrasilServer extends HttpServer { } private Response profiles(Request request) throws IOException { - String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), StandardCharsets.UTF_8); + String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), UTF_8); List names = JsonUtils.fromNonNullJson(body, new TypeToken>() { }.getType()); return ok(names.stream().distinct() @@ -195,7 +200,7 @@ public class YggdrasilServer extends HttpServer { return existent; } - String url = String.format("http://127.0.0.1:%d/textures/%s", getListeningPort(), hash); + String url = String.format("http://localhost:%d/textures/%s", getListeningPort(), hash); ByteArrayOutputStream buf = new ByteArrayOutputStream(); ImageIO.write(img, "png", buf); Texture texture = new Texture(hash, buf.toByteArray(), url); @@ -218,28 +223,13 @@ public class YggdrasilServer extends HttpServer { charactersByName.put(character.getName(), character); } - public enum ModelType { - STEVE("default"), - ALEX("slim"); - - private String modelName; - - ModelType(String modelName) { - this.modelName = modelName; - } - - public String getModelName() { - return modelName; - } - } - public static class Character { private final UUID uuid; private final String name; - private final ModelType model; + private final TextureModel model; private final Map textures; - public Character(UUID uuid, String name, ModelType model, Map textures) { + public Character(UUID uuid, String name, TextureModel model, Map textures) { this.uuid = uuid; this.name = name; this.model = model; @@ -254,7 +244,7 @@ public class YggdrasilServer extends HttpServer { return name; } - public ModelType getModel() { + public TextureModel getModel() { return model; } @@ -273,11 +263,11 @@ public class YggdrasilServer extends HttpServer { return new GameProfile(uuid, name); } - public CompleteGameProfile toCompleteResponse(String rootUrl) { - Map realTextures = new HashMap<>(); + public Object toCompleteResponse(String rootUrl) { + Map realTextures = new HashMap<>(); for (Map.Entry textureEntry : textures.entrySet()) { if (textureEntry.getValue() == null) continue; - realTextures.put(textureEntry.getKey(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().hash))); + realTextures.put(textureEntry.getKey().name(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().hash))); } Map textureResponse = mapOf( @@ -287,13 +277,15 @@ public class YggdrasilServer extends HttpServer { pair("textures", realTextures) ); - return new CompleteGameProfile(uuid, name, mapOf( - pair("textures", new String( - Base64.getEncoder().encode( - JsonUtils.GSON.toJson(textureResponse).getBytes(StandardCharsets.UTF_8) - ), StandardCharsets.UTF_8) - ) - )); + return mapOf( + pair("id", uuid), + pair("name", name), + pair("properties", properties(true, + pair("textures", new String( + Base64.getEncoder().encode( + JsonUtils.GSON.toJson(textureResponse).getBytes(UTF_8) + ), UTF_8)))) + ); } } @@ -321,4 +313,45 @@ public class YggdrasilServer extends HttpServer { } } + // === Signature === + + private static final KeyPair keyPair = KeyUtils.generateKey(); + + public static PublicKey getSignaturePublicKey() { + return keyPair.getPublic(); + } + + private static String sign(String data) { + try { + Signature signature = Signature.getInstance("SHA1withRSA"); + signature.initSign(keyPair.getPrivate(), new SecureRandom()); + signature.update(data.getBytes(UTF_8)); + return Base64.getEncoder().encodeToString(signature.sign()); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + // === properties === + + @SafeVarargs + public static List properties(Map.Entry... entries) { + return properties(false, entries); + } + + @SafeVarargs + public static List properties(boolean sign, Map.Entry... entries) { + return Stream.of(entries) + .map(entry -> { + LinkedHashMap property = new LinkedHashMap<>(); + property.put("name", entry.getKey()); + property.put("value", entry.getValue()); + if (sign) { + property.put("signature", sign(entry.getValue())); + } + return property; + }) + .collect(Collectors.toList()); + } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyUtils.java new file mode 100644 index 000000000..341cc37be --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyUtils.java @@ -0,0 +1,43 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util; + +import java.security.*; +import java.util.Base64; + +public final class KeyUtils { + private KeyUtils() { + } + + public static KeyPair generateKey() { + try { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(4096, new SecureRandom()); + return gen.genKeyPair(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + public static String toPEMPublicKey(PublicKey key) { + byte[] encoded = key.getEncoded(); + return "-----BEGIN PUBLIC KEY-----\n" + + Base64.getMimeEncoder(76, new byte[]{'\n'}).encodeToString(encoded) + + "\n-----END PUBLIC KEY-----\n"; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpServer.java index 7ae903005..541af0253 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpServer.java @@ -46,7 +46,7 @@ public class HttpServer extends NanoHTTPD { } public String getRootUrl() { - return "http://127.0.0.1:" + getListeningPort(); + return "http://localhost:" + getListeningPort(); } protected void addRoute(Method method, Pattern path, ExceptionalFunction server) {