From 68d7c0a947d58a302af3b27372f4171ccf0a5db8 Mon Sep 17 00:00:00 2001 From: Gravita Date: Mon, 26 Apr 2021 22:21:28 +0700 Subject: [PATCH] [FEATURE] Support encrypted runtime --- .../launchserver/binary/BuildContext.java | 61 +++++++++++++++++++ .../binary/tasks/MainBuildTask.java | 10 ++- .../config/LaunchServerConfig.java | 1 + .../config/LaunchServerRuntimeConfig.java | 2 + .../pro/gravit/launcher/LauncherConfig.java | 43 +++++++++++-- .../profiles/optional/OptionalDepend.java | 1 + .../profiles/optional/OptionalFile.java | 7 +++ .../profiles/optional/OptionalType.java | 1 + .../profiles/optional/OptionalView.java | 57 +++++++++++++++++ .../gravit/utils/helper/SecurityHelper.java | 8 +-- .../gravit/launcher/server/ServerWrapper.java | 11 +--- 11 files changed, 180 insertions(+), 22 deletions(-) diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/BuildContext.java b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/BuildContext.java index 4d2640e6..46a0c75b 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/BuildContext.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/BuildContext.java @@ -8,6 +8,10 @@ import pro.gravit.utils.helper.LogHelper; import pro.gravit.utils.helper.SecurityHelper; +import javax.crypto.Cipher; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; @@ -16,6 +20,8 @@ import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -70,6 +76,10 @@ public void pushDir(Path dir, String targetDir, Map hashMap, boo IOHelper.walk(dir, new RuntimeDirVisitor(output, hashMap, dir, targetDir), hidden); } + public void pushEncryptedDir(Path dir, String targetDir, String aesHexKey, Map hashMap, boolean hidden) throws IOException { + IOHelper.walk(dir, new EncryptedRuntimeDirVisitor(output, aesHexKey, hashMap, dir, targetDir), hidden); + } + public void pushBytes(String filename, byte[] bytes) throws IOException { ZipEntry zip = IOHelper.newZipEntry(filename); output.putNextEntry(zip); @@ -178,4 +188,55 @@ private ZipEntry newEntry(String fileName) { return newZipEntry(targetDir + IOHelper.CROSS_SEPARATOR + fileName); } } + + private final static class EncryptedRuntimeDirVisitor extends SimpleFileVisitor { + private final ZipOutputStream output; + private final Map hashs; + private final Path sourceDir; + private final String targetDir; + private final SecretKeySpec sKeySpec; + + private EncryptedRuntimeDirVisitor(ZipOutputStream output, String aesKey, Map hashs, Path sourceDir, String targetDir) { + this.output = output; + this.hashs = hashs; + this.sourceDir = sourceDir; + this.targetDir = targetDir; + try { + byte[] key = SecurityHelper.fromHex(aesKey); + byte[] compatKey = SecurityHelper.getAESKey(key); + sKeySpec = new SecretKeySpec(compatKey, "AES"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + byte[] digest = SecurityHelper.digest(SecurityHelper.DigestAlgorithm.MD5, file); + String fileName = IOHelper.toString(sourceDir.relativize(file)); + if (hashs != null) + hashs.put(fileName, digest); + + // Create zip entry and transfer contents + output.putNextEntry(newEntry(SecurityHelper.toHex(digest))); + + + Cipher cipher = null; + try { + cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, sKeySpec); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) { + throw new RuntimeException(e); + } + + IOHelper.transfer(file, new CipherOutputStream(output, cipher)); + + // Return result + return super.visitFile(file, attrs); + } + + private ZipEntry newEntry(String fileName) { + return newZipEntry(targetDir + IOHelper.CROSS_SEPARATOR + fileName); + } + } } diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/tasks/MainBuildTask.java b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/tasks/MainBuildTask.java index 24670670..553dd7cb 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/tasks/MainBuildTask.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/tasks/MainBuildTask.java @@ -69,10 +69,14 @@ public Path process(Path inputJar) throws IOException { // map for guard Map runtime = new HashMap<>(256); // Write launcher guard dir - context.pushDir(server.launcherBinary.runtimeDir, Launcher.RUNTIME_DIR, runtime, false); + if(server.config.launcher.encryptRuntime) { + context.pushEncryptedDir(server.launcherBinary.runtimeDir, Launcher.RUNTIME_DIR, server.runtime.runtimeEncryptKey, runtime, false); + } else { + context.pushDir(server.launcherBinary.runtimeDir, Launcher.RUNTIME_DIR, runtime, false); + } context.pushDir(server.launcherBinary.guardDir, Launcher.GUARD_DIR, runtime, false); - LauncherConfig launcherConfig = new LauncherConfig(server.config.netty.address, server.publicKey, runtime, server.config.projectName); + LauncherConfig launcherConfig = new LauncherConfig(server.config.netty.address, server.keyAgreementManager.ecdsaPublicKey, server.keyAgreementManager.rsaPublicKey, runtime, server.config.projectName); context.pushFile(Launcher.CONFIG_FILE, launcherConfig); postBuildHook.hook(context); } @@ -109,6 +113,8 @@ protected void initProps() { properties.put("launcher.guardType", server.config.launcher.guardType); properties.put("launchercore.env", server.config.env); properties.put("launcher.memory", server.config.launcher.memoryLimit); + if (server.runtime.runtimeEncryptKey == null) server.runtime.runtimeEncryptKey= SecurityHelper.randomStringAESKey(); + properties.put("launcher.runtimeEncryptKey", server.runtime.runtimeEncryptKey); properties.put("launcher.certificatePinning", server.config.launcher.certificatePinning); properties.put("runtimeconfig.passwordEncryptKey", server.runtime.passwordEncryptKey); String launcherSalt = SecurityHelper.randomStringToken(); diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java b/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java index af4f2208..d8ff08dc 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java @@ -299,6 +299,7 @@ public static class LauncherConf { public boolean stripLineNumbers; public boolean deleteTempFiles; public boolean certificatePinning; + public boolean encryptRuntime; public int memoryLimit = 256; } diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerRuntimeConfig.java b/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerRuntimeConfig.java index c688e10b..51ccece6 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerRuntimeConfig.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerRuntimeConfig.java @@ -5,6 +5,7 @@ public class LaunchServerRuntimeConfig { public String passwordEncryptKey; + public String runtimeEncryptKey; public String oemUnlockKey; public String registerApiKey; public String clientCheckSecret; @@ -19,6 +20,7 @@ public void verify() { public void reset() { passwordEncryptKey = SecurityHelper.randomStringToken(); + runtimeEncryptKey = SecurityHelper.randomStringAESKey(); registerApiKey = SecurityHelper.randomStringToken(); clientCheckSecret = SecurityHelper.randomStringToken(); } diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java b/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java index 17763824..9060ea67 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java @@ -14,6 +14,7 @@ import java.lang.invoke.MethodType; import java.security.cert.CertificateException; import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.util.*; @@ -28,7 +29,10 @@ public final class LauncherConfig extends StreamObject { @LauncherInject("launcher.port") public final int clientPort; public final LauncherTrustManager trustManager; - public final ECPublicKey publicKey; + @Deprecated + public ECPublicKey publicKey = null; + public final ECPublicKey ecdsaPublicKey; + public final RSAPublicKey rsaPublicKey; public final Map runtime; @Deprecated public final boolean isWarningMissArchJava; @@ -40,8 +44,10 @@ public final class LauncherConfig extends StreamObject { public final String secureCheckSalt; @LauncherInject("runtimeconfig.passwordEncryptKey") public final String passwordEncryptKey; + @LauncherInject("runtimeconfig.runtimeEncryptKey") + public final String runtimeEncryptKey; @LauncherInject("launcher.address") - public String address; + public final String address; @LauncherInject("runtimeconfig.secretKeyClient") public String secretKeyClient; @LauncherInject("runtimeconfig.oemUnlockKey") @@ -52,10 +58,13 @@ public final class LauncherConfig extends StreamObject { @LauncherInjectionConstructor public LauncherConfig(HInput input) throws IOException, InvalidKeySpecException { - publicKey = SecurityHelper.toPublicECDSAKey(input.readByteArray(SecurityHelper.CRYPTO_MAX_LENGTH)); + ecdsaPublicKey = SecurityHelper.toPublicECDSAKey(input.readByteArray(SecurityHelper.CRYPTO_MAX_LENGTH)); + rsaPublicKey = SecurityHelper.toPublicRSAKey(input.readByteArray(SecurityHelper.CRYPTO_MAX_LENGTH)); + publicKey = ecdsaPublicKey; secureCheckHash = null; secureCheckSalt = null; passwordEncryptKey = null; + runtimeEncryptKey = null; projectName = null; clientPort = -1; secretKeyClient = null; @@ -83,9 +92,12 @@ public LauncherConfig(HInput input) throws IOException, InvalidKeySpecException runtime = Collections.unmodifiableMap(localResources); } + @Deprecated public LauncherConfig(String address, ECPublicKey publicKey, Map runtime, String projectName) { this.address = address; this.publicKey = publicKey; + this.ecdsaPublicKey = this.publicKey; + this.rsaPublicKey = null; this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime)); this.projectName = projectName; this.clientPort = 32148; @@ -95,6 +107,24 @@ public LauncherConfig(String address, ECPublicKey publicKey, Map secureCheckSalt = null; secureCheckHash = null; passwordEncryptKey = null; + runtimeEncryptKey = null; + trustManager = null; + } + + public LauncherConfig(String address, ECPublicKey ecdsaPublicKey, RSAPublicKey rsaPublicKey, Map runtime, String projectName) { + this.address = address; + this.ecdsaPublicKey = ecdsaPublicKey; + this.rsaPublicKey = rsaPublicKey; + this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime)); + this.projectName = projectName; + this.clientPort = 32148; + guardType = "no"; + isWarningMissArchJava = true; + environment = LauncherEnvironment.STD; + secureCheckSalt = null; + secureCheckHash = null; + passwordEncryptKey = null; + runtimeEncryptKey = null; trustManager = null; } @@ -103,14 +133,16 @@ public LauncherConfig(String address, Map runtime, String projec this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime)); this.projectName = projectName; this.clientPort = 32148; - this.publicKey = null; this.trustManager = trustManager; + this.rsaPublicKey = null; + this.ecdsaPublicKey = null; environment = env; guardType = "no"; isWarningMissArchJava = true; secureCheckSalt = null; secureCheckHash = null; passwordEncryptKey = null; + runtimeEncryptKey = null; } public static void initModules(LauncherModulesManager modulesManager) { @@ -126,7 +158,8 @@ public static void initModules(LauncherModulesManager modulesManager) { @Override public void write(HOutput output) throws IOException { - output.writeByteArray(publicKey.getEncoded(), SecurityHelper.CRYPTO_MAX_LENGTH); + output.writeByteArray(ecdsaPublicKey.getEncoded(), SecurityHelper.CRYPTO_MAX_LENGTH); + output.writeByteArray(rsaPublicKey.getEncoded(), SecurityHelper.CRYPTO_MAX_LENGTH); // Write signed runtime Set> entrySet = runtime.entrySet(); diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalDepend.java b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalDepend.java index 3283fc03..b8e934f7 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalDepend.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalDepend.java @@ -5,6 +5,7 @@ public class OptionalDepend { @LauncherNetworkAPI public String name; + @Deprecated @LauncherNetworkAPI public OptionalType type; } diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalFile.java b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalFile.java index 373032a8..e1253053 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalFile.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalFile.java @@ -49,8 +49,10 @@ public class OptionalFile { public boolean isPreset; @Deprecated public transient Set dependenciesCount; + @Deprecated private volatile transient Collection> watchList = null; + @Deprecated public static OptionalType readType(HInput input) throws IOException { int t = input.readInt(); OptionalType type; @@ -87,6 +89,7 @@ public int hashCode() { return Objects.hash(name); } + @Deprecated public OptionalType getType() { return OptionalType.FILE; } @@ -128,21 +131,25 @@ public void writeType(HOutput output) throws IOException { } } + @Deprecated public void registerWatcher(BiConsumer watcher) { if (watchList == null) watchList = ConcurrentHashMap.newKeySet(); watchList.add(watcher); } + @Deprecated public void removeWatcher(BiConsumer watcher) { if (watchList == null) return; watchList.remove(watcher); } + @Deprecated public void clearAllWatchers() { if (watchList == null) return; watchList.clear(); } + @Deprecated public void watchEvent(boolean isMark) { if (watchList == null) return; watchList.forEach((e) -> e.accept(this, isMark)); diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalType.java b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalType.java index 875dc809..5e1be92d 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalType.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalType.java @@ -2,6 +2,7 @@ import pro.gravit.launcher.LauncherNetworkAPI; +@Deprecated public enum OptionalType { @LauncherNetworkAPI FILE, diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalView.java b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalView.java index a576c867..b21080d7 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalView.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/optional/OptionalView.java @@ -7,11 +7,17 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; public class OptionalView { public Set enabled = new HashSet<>(); + @Deprecated public Map> dependenciesCountMap = new HashMap<>(); + public Map installInfo = new HashMap<>(); public Set all; + public static class OptionalFileInstallInfo { + public boolean isManual; + } public OptionalView(ClientProfile profile) { this.all = profile.getOptional(); @@ -23,6 +29,7 @@ public OptionalView(ClientProfile profile) { public OptionalView(OptionalView view) { this.enabled = new HashSet<>(view.enabled); this.dependenciesCountMap = new HashMap<>(view.dependenciesCountMap); + this.installInfo = new HashMap<>(view.installInfo); this.all = view.all; } @@ -62,6 +69,7 @@ public Set getDisabledActions() { return results; } + @Deprecated public void enable(OptionalFile file) { if (enabled.contains(file)) return; enabled.add(file); @@ -80,6 +88,7 @@ public void enable(OptionalFile file) { } } + @Deprecated public void disable(OptionalFile file) { if (!enabled.remove(file)) return; file.watchEvent(false); @@ -104,4 +113,52 @@ public void disable(OptionalFile file) { } } } + + public void enable(OptionalFile file, boolean manual, BiConsumer callback) { + if(enabled.contains(file)) return; + if(callback != null) callback.accept(file, true); + OptionalFileInstallInfo installInfo = this.installInfo.get(file); + if(installInfo == null) { + installInfo = new OptionalFileInstallInfo(); + this.installInfo.put(file, installInfo); + } + installInfo.isManual = manual; + if (file.dependencies != null) { + for (OptionalFile dep : file.dependencies) { + enable(dep, false, callback); + } + } + if (file.conflict != null) { + for (OptionalFile conflict : file.conflict) { + disable(conflict); + } + } + } + + public void disable(OptionalFile file, BiConsumer callback) { + if(!enabled.remove(file)) return; + if(callback != null) callback.accept(file, false); + for(OptionalFile dep : all) { + if(dep.dependencies != null && contains(file, dep.dependencies)) { + disable(dep, callback); + } + } + if (file.dependencies != null) { + for (OptionalFile dep : file.dependencies) { + OptionalFileInstallInfo installInfo = this.installInfo.get(dep); + if(installInfo != null && !installInfo.isManual) { + disable(file, callback); + } + } + } + } + + private boolean contains(OptionalFile file, OptionalFile[] array) { + for(OptionalFile e : array) { + if(e == file) { + return true; + } + } + return false; + } } diff --git a/LauncherCore/src/main/java/pro/gravit/utils/helper/SecurityHelper.java b/LauncherCore/src/main/java/pro/gravit/utils/helper/SecurityHelper.java index 0221741b..639a1446 100644 --- a/LauncherCore/src/main/java/pro/gravit/utils/helper/SecurityHelper.java +++ b/LauncherCore/src/main/java/pro/gravit/utils/helper/SecurityHelper.java @@ -1,7 +1,5 @@ package pro.gravit.utils.helper; -import org.bouncycastle.jcajce.provider.asymmetric.RSA; - import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; @@ -489,7 +487,7 @@ public static Cipher newRSAEncryptCipher(RSAPublicKey publicKey) { //AES public static byte[] encrypt(String seed, byte[] cleartext) throws Exception { - byte[] rawKey = getRawKey(seed.getBytes()); + byte[] rawKey = getAESKey(seed.getBytes()); return encrypt(rawKey, cleartext); } @@ -497,7 +495,7 @@ public static byte[] encrypt(String seed, String cleartext) throws Exception { return encrypt(seed, cleartext.getBytes()); } - private static byte[] getRawKey(byte[] seed) throws Exception { + public static byte[] getAESKey(byte[] seed) throws Exception { KeyGenerator kGen = KeyGenerator.getInstance("AES"); SecureRandom sr = SecureRandom.getInstance("SHA1PRNG"); sr.setSeed(seed); @@ -521,7 +519,7 @@ public static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception { } public static byte[] decrypt(String seed, byte[] encrypted) throws Exception { - return decrypt(getRawKey(seed.getBytes()), encrypted); + return decrypt(getAESKey(seed.getBytes()), encrypted); } public static byte[] fromHex(String hexString) { diff --git a/ServerWrapper/src/main/java/pro/gravit/launcher/server/ServerWrapper.java b/ServerWrapper/src/main/java/pro/gravit/launcher/server/ServerWrapper.java index 78485fb1..168c6263 100644 --- a/ServerWrapper/src/main/java/pro/gravit/launcher/server/ServerWrapper.java +++ b/ServerWrapper/src/main/java/pro/gravit/launcher/server/ServerWrapper.java @@ -215,16 +215,7 @@ public void run(String... args) throws Throwable { public void updateLauncherConfig() { - LauncherConfig cfg = null; - try { - ECPublicKey publicKey = null; - if (IOHelper.isFile(publicKeyFile)) - publicKey = SecurityHelper.toPublicECKey(IOHelper.read(publicKeyFile)); - cfg = new LauncherConfig(config.address, publicKey, new HashMap<>(), config.projectname); - cfg.address = config.address; - } catch (InvalidKeySpecException | IOException e) { - LogHelper.error(e); - } + LauncherConfig cfg = new LauncherConfig(config.address, null, null, new HashMap<>(), config.projectname); Launcher.setConfig(cfg); }