From 4592eee9532db08089d433ad962e0a9341703c63 Mon Sep 17 00:00:00 2001 From: Gravita Date: Fri, 29 Jan 2021 00:13:21 +0700 Subject: [PATCH] [FEATURE] CertificatePinning, AGENT, min/max/recommend Java and other --- .../binary/tasks/MainBuildTask.java | 3 +- .../config/LaunchServerConfig.java | 4 +- Launcher/build.gradle | 6 +- .../launcher/ClientLauncherWrapper.java | 2 + .../client/ClientLauncherEntryPoint.java | 106 ++++++++++++------ .../client/ClientLauncherProcess.java | 5 +- .../pro/gravit/launcher/LauncherConfig.java | 2 +- .../launcher/profiles/ClientProfile.java | 54 +++++++++ .../request/websockets/ClientJSONPoint.java | 21 +++- .../pro/gravit/launcher/AsyncDownloader.java | 33 ++++++ .../CertificatePinningTrustManager.java | 62 ++++++++++ .../main/java/pro/gravit/utils/Version.java | 2 +- 12 files changed, 254 insertions(+), 46 deletions(-) create mode 100644 LauncherCore/src/main/java/pro/gravit/launcher/CertificatePinningTrustManager.java 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 d6f768e1..19c1775b 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 @@ -107,8 +107,9 @@ protected void initProps() { properties.put("runtimeconfig.secretKeyClient", SecurityHelper.randomStringAESKey()); properties.put("launcher.port", 32148 + SecurityHelper.newRandom().nextInt(512)); properties.put("launcher.guardType", server.config.launcher.guardType); - properties.put("launcher.isWarningMissArchJava", server.config.launcher.warningMissArchJava); properties.put("launchercore.env", server.config.env); + properties.put("launcher.memory", server.config.launcher.memoryLimit); + properties.put("launcher.certificatePinning", server.config.launcher.certificatePinning); properties.put("runtimeconfig.passwordEncryptKey", server.runtime.passwordEncryptKey); String launcherSalt = SecurityHelper.randomStringToken(); byte[] launcherSecureHash = SecurityHelper.digest(SecurityHelper.DigestAlgorithm.SHA256, 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 170cc4c4..671a4ba9 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java @@ -93,7 +93,6 @@ public static LaunchServerConfig getDefault(LaunchServer.LaunchServerEnv env) { newConfig.launcher = new LauncherConf(); newConfig.launcher.guardType = "no"; newConfig.launcher.compress = true; - newConfig.launcher.warningMissArchJava = true; newConfig.launcher.attachLibraryBeforeProGuard = false; newConfig.launcher.deleteTempFiles = true; newConfig.launcher.enabledProGuard = true; @@ -289,11 +288,14 @@ public static class LauncherConf { public String guardType; public boolean attachLibraryBeforeProGuard; public boolean compress; + @Deprecated public boolean warningMissArchJava; public boolean enabledProGuard; public boolean stripLineNumbers; public boolean deleteTempFiles; public boolean proguardGenMappings; + public boolean certificatePinning; + public int memoryLimit = 256; } public static class NettyConfig { diff --git a/Launcher/build.gradle b/Launcher/build.gradle index ca8c804a..9ecd01cc 100644 --- a/Launcher/build.gradle +++ b/Launcher/build.gradle @@ -26,11 +26,7 @@ classifier = 'clean' manifest.attributes("Main-Class": mainClassName, "Premain-Class": mainAgentName, - "Can-Redefine-Classes": "true", - "Multi-Release": "true", - "Can-Retransform-Classes": "true", - "Can-Set-Native-Method-Prefix": "true", - "Multi-Release-Jar": "true") + "Multi-Release": "true") } task sourcesJar(type: Jar) { diff --git a/Launcher/src/main/java/pro/gravit/launcher/ClientLauncherWrapper.java b/Launcher/src/main/java/pro/gravit/launcher/ClientLauncherWrapper.java index 94edcf82..317fab67 100644 --- a/Launcher/src/main/java/pro/gravit/launcher/ClientLauncherWrapper.java +++ b/Launcher/src/main/java/pro/gravit/launcher/ClientLauncherWrapper.java @@ -22,6 +22,8 @@ public class ClientLauncherWrapper { public static final String NO_JAVA_CHECK_PROPERTY = "launcher.noJavaCheck"; public static boolean noJavaCheck = Boolean.getBoolean(NO_JAVA_CHECK_PROPERTY); public static boolean waitProcess = Boolean.getBoolean(WAIT_PROCESS_PROPERTY); + @LauncherInject("launcher.memory") + public static int launcherMemoryLimit; public static class JavaVersion { public final Path jvmDir; diff --git a/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherEntryPoint.java b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherEntryPoint.java index 00001648..337f8453 100644 --- a/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherEntryPoint.java +++ b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherEntryPoint.java @@ -10,6 +10,7 @@ import pro.gravit.launcher.guard.LauncherGuardManager; import pro.gravit.launcher.hasher.FileNameMatcher; import pro.gravit.launcher.hasher.HashedDir; +import pro.gravit.launcher.hasher.HashedEntry; import pro.gravit.launcher.managers.ClientGsonManager; import pro.gravit.launcher.modules.events.PreConfigPhase; import pro.gravit.launcher.patches.FMLPatcher; @@ -26,25 +27,24 @@ import pro.gravit.utils.helper.*; import javax.swing.*; +import java.io.File; import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.URL; +import java.net.*; import java.nio.file.FileVisitResult; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.stream.Stream; public class ClientLauncherEntryPoint { - private static ClientClassLoader classLoader; + private static ClassLoader classLoader; private static ClientLauncherProcess.ClientParams readParams(SocketAddress address) throws IOException { try (Socket socket = IOHelper.newSocket()) { @@ -77,18 +77,20 @@ public static void main(String[] args) throws Throwable { LauncherConfig.initModules(LauncherEngine.modulesManager); //INIT LauncherEngine.modulesManager.initModules(null); initGson(LauncherEngine.modulesManager); - LauncherEngine.verifyNoAgent(); LauncherEngine.modulesManager.invokeEvent(new PreConfigPhase()); engine.readKeys(); LauncherGuardManager.initGuard(true); LogHelper.debug("Reading ClientLauncher params"); ClientLauncherProcess.ClientParams params = readParams(new InetSocketAddress("127.0.0.1", Launcher.getConfig().clientPort)); + if(params.profile.classLoaderConfig != ClientProfile.ClassLoaderConfig.AGENT) { + LauncherEngine.verifyNoAgent(); + } ClientProfile profile = params.profile; Launcher.profile = profile; AuthService.profile = profile; LauncherEngine.clientParams = params; Request.setSession(params.session); - checkJVMBitsAndVersion(); + checkJVMBitsAndVersion(params.profile.getMinJavaVersion(), params.profile.getRecommendJavaVersion(), params.profile.getMaxJavaVersion(), params.profile.isWarnMissJavaVersion()); LauncherEngine.modulesManager.invokeEvent(new ClientProcessInitPhase(engine, params)); Path clientDir = Paths.get(params.clientDir); @@ -103,10 +105,6 @@ public static void main(String[] args) throws Throwable { if (a instanceof OptionalActionClassPath) resolveClassPathStream(clientDir, ((OptionalActionClassPath) a).args).map(IOHelper::toURL).collect(Collectors.toCollection(() -> classpath)); } - classLoader = new ClientClassLoader(classpath.toArray(new URL[0]), ClassLoader.getSystemClassLoader()); - Thread.currentThread().setContextClassLoader(classLoader); - classLoader.nativePath = clientDir.resolve("natives").toString(); - LauncherEngine.modulesManager.invokeEvent(new ClientProcessClassLoaderEvent(engine, classLoader, profile)); // Start client with WatchService monitoring boolean digest = !profile.isUpdateFastCheck(); LogHelper.debug("Restore sessions"); @@ -129,13 +127,34 @@ public static void main(String[] args) throws Throwable { LogHelper.error(e); } }; - AuthService.username = params.playerProfile.username; - AuthService.uuid = params.playerProfile.uuid; - ClientService.classLoader = classLoader; - ClientService.nativePath = classLoader.nativePath; - classLoader.addURL(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toUri().toURL()); - //classForName(classLoader, "com.google.common.collect.ForwardingMultimap"); - ClientService.baseURLs = classLoader.getURLs(); + if(params.profile.classLoaderConfig == ClientProfile.ClassLoaderConfig.LAUNCHER) { + ClientClassLoader classLoader = new ClientClassLoader(classpath.toArray(new URL[0]), ClassLoader.getSystemClassLoader()); + ClientLauncherEntryPoint.classLoader = classLoader; + Thread.currentThread().setContextClassLoader(classLoader); + classLoader.nativePath = clientDir.resolve("natives").toString(); + LauncherEngine.modulesManager.invokeEvent(new ClientProcessClassLoaderEvent(engine, classLoader, profile)); + AuthService.username = params.playerProfile.username; + AuthService.uuid = params.playerProfile.uuid; + ClientService.classLoader = classLoader; + ClientService.nativePath = classLoader.nativePath; + classLoader.addURL(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toUri().toURL()); + ClientService.baseURLs = classLoader.getURLs(); + } + else if(params.profile.classLoaderConfig == ClientProfile.ClassLoaderConfig.AGENT) { + ClientLauncherEntryPoint.classLoader = ClassLoader.getSystemClassLoader(); + classpath.add(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toUri().toURL()); + for(URL url : classpath) { + LauncherAgent.addJVMClassPath(Paths.get(url.toURI())); + } + ClientService.instrumentation = LauncherAgent.inst; + ClientService.nativePath = clientDir.resolve("natives").toString(); + LauncherEngine.modulesManager.invokeEvent(new ClientProcessClassLoaderEvent(engine, classLoader, profile)); + AuthService.username = params.playerProfile.username; + AuthService.uuid = params.playerProfile.uuid; + ClientService.classLoader = classLoader; + ClientService.baseURLs = classpath.toArray(new URL[0]); + } + LauncherEngine.modulesManager.invokeEvent(new ClientProcessReadyEvent(engine, params)); LogHelper.debug("Starting JVM and client WatchService"); FileNameMatcher assetMatcher = profile.getAssetUpdateMatcher(); @@ -179,35 +198,42 @@ public static void verifyHDir(Path dir, HashedDir hdir, FileNameMatcher matcher, HashedDir currentHDir = new HashedDir(dir, matcher, true, digest); HashedDir.Diff diff = hdir.diff(currentHDir, matcher); if (!diff.isSame()) { - /*AtomicBoolean isFoundFile = new AtomicBoolean(false); - diff.extra.walk(File.separator, (e,k,v) -> { - if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Extra file %s", e); isFoundFile.set(true); } - else LogHelper.error("Extra %s", e); - }); - diff.mismatch.walk(File.separator, (e,k,v) -> { - if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Mismatch file %s", e); isFoundFile.set(true); } - else LogHelper.error("Mismatch %s", e); - }); - if(isFoundFile.get())*/ + if(LogHelper.isDebugEnabled()) { + diff.extra.walk(File.separator, (e, k, v) -> { + if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Extra file %s", e); } + else LogHelper.error("Extra %s", e); + return HashedDir.WalkAction.CONTINUE; + }); + diff.mismatch.walk(File.separator, (e,k,v) -> { + if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Mismatch file %s", e); } + else LogHelper.error("Mismatch %s", e); + return HashedDir.WalkAction.CONTINUE; + }); + } throw new SecurityException(String.format("Forbidden modification: '%s'", IOHelper.getFileName(dir))); } } - public static void checkJVMBitsAndVersion() { + public static boolean checkJVMBitsAndVersion(int minVersion, int recommendVersion, int maxVersion, boolean showMessage) { + boolean ok = true; if (JVMHelper.JVM_BITS != JVMHelper.OS_BITS) { String error = String.format("У Вас установлена Java %d, но Ваша система определена как %d. Установите Java правильной разрядности", JVMHelper.JVM_BITS, JVMHelper.OS_BITS); LogHelper.error(error); - if (Launcher.getConfig().isWarningMissArchJava) + if (showMessage) JOptionPane.showMessageDialog(null, error); + ok = false; } String jvmVersion = JVMHelper.RUNTIME_MXBEAN.getVmVersion(); LogHelper.info(jvmVersion); - if (jvmVersion.startsWith("10.") || jvmVersion.startsWith("9.") || jvmVersion.startsWith("11.")) { - String error = String.format("У Вас установлена Java %s. Для правильной работы необходима Java 8", JVMHelper.RUNTIME_MXBEAN.getVmVersion()); + int version = JVMHelper.getVersion(); + if (version < minVersion || version > maxVersion) { + String error = String.format("У Вас установлена Java %s. Для правильной работы необходима Java %d", JVMHelper.RUNTIME_MXBEAN.getVmVersion(), recommendVersion); LogHelper.error(error); - if (Launcher.getConfig().isWarningMissArchJava) + if (showMessage) JOptionPane.showMessageDialog(null, error); + ok = false; } + return ok; } private static LinkedList resolveClassPathList(Path clientDir, String... classPath) throws IOException { @@ -253,11 +279,21 @@ private static void launch(ClientProfile profile, ClientLauncherProcess.ClientPa LogHelper.debug("Args: " + copy); // Resolve main class and method Class mainClass = classLoader.loadClass(profile.getMainClass()); - for (URL u : classLoader.getURLs()) { - LogHelper.info("ClassLoader URL: %s", u.toString()); + if(LogHelper.isDevEnabled() && classLoader instanceof URLClassLoader) { + for (URL u : ((URLClassLoader)classLoader).getURLs()) { + LogHelper.dev("ClassLoader URL: %s", u.toString()); + } } FMLPatcher.apply(); LauncherEngine.modulesManager.invokeEvent(new ClientProcessPreInvokeMainClassEvent(params, profile, args)); + { + List compatClasses = profile.getCompatClasses(); + for(String e : compatClasses) { + Class clazz = classLoader.loadClass(e); + MethodHandle runMethod = MethodHandles.publicLookup().findStatic(clazz, "run", MethodType.methodType(void.class)); + runMethod.invoke(); + } + } MethodHandle mainMethod = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class)).asFixedArity(); Launcher.LAUNCHED.set(true); JVMHelper.fullGC(); diff --git a/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherProcess.java b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherProcess.java index 8b05806a..cb064f92 100644 --- a/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherProcess.java +++ b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherProcess.java @@ -117,6 +117,9 @@ public void start(boolean pipeOutput) throws IOException, InterruptedException { processArgs.add(executeFile.toString()); processArgs.addAll(jvmArgs); //ADD CLASSPATH + if(params.profile.classLoaderConfig == ClientProfile.ClassLoaderConfig.AGENT) { + processArgs.add("-javaagent:".concat(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toAbsolutePath().toString())); + } if (useLegacyJavaClassPathProperty) { processArgs.add("-Djava.class.path=".concat(String.join(getPathSeparator(), systemClassPath))); } else { @@ -160,7 +163,7 @@ public void runWriteParams(SocketAddress address) throws IOException { output.writeByteArray(serializedMainParams, 0); params.clientHDir.write(output); params.assetHDir.write(output); - if (params.javaHDir == null || params.javaHDir == params.assetHDir) { //TODO: OLD RUNTIME USE params.assetHDir AS NULL IN java.javaHDir + if (params.javaHDir == null || params.javaHDir == params.assetHDir) { //OLD RUNTIME USE params.assetHDir AS NULL IN java.javaHDir output.writeBoolean(false); } else { output.writeBoolean(true); diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java b/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java index 3ad49fcb..77276ea4 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/LauncherConfig.java @@ -30,7 +30,7 @@ public final class LauncherConfig extends StreamObject { public final LauncherTrustManager trustManager; public final ECPublicKey publicKey; public final Map runtime; - @LauncherInject("launcher.isWarningMissArchJava") + @Deprecated public final boolean isWarningMissArchJava; @LauncherInject("launcher.guardType") public final String guardType; diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java index b5a2e87c..9338aae9 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java @@ -42,6 +42,8 @@ public final class ClientProfile implements Comparable { public SecurityManagerConfig securityManagerConfig = SecurityManagerConfig.CLIENT; @LauncherNetworkAPI public ClassLoaderConfig classLoaderConfig = ClassLoaderConfig.LAUNCHER; + @LauncherNetworkAPI + public SignedClientConfig signedClientConfig = SignedClientConfig.NONE; // Version @LauncherNetworkAPI private String version; @@ -51,6 +53,14 @@ public final class ClientProfile implements Comparable { private String dir; @LauncherNetworkAPI private String assetDir; + @LauncherNetworkAPI + private int recommendJavaVersion = 8; + @LauncherNetworkAPI + private int minJavaVersion = 8; + @LauncherNetworkAPI + private int maxJavaVersion = 17; + @LauncherNetworkAPI + private boolean warnMissJavaVersion = true; // Client @LauncherNetworkAPI private int sortIndex; @@ -72,6 +82,8 @@ public final class ClientProfile implements Comparable { @LauncherNetworkAPI private String mainClass; @LauncherNetworkAPI + private final List compatClasses = new ArrayList<>(); + @LauncherNetworkAPI private final Map properties = new HashMap<>(); public static class ServerProfile { @@ -173,6 +185,38 @@ public Set getOptional() { return updateOptional; } + public int getRecommendJavaVersion() { + return recommendJavaVersion; + } + + public void setRecommendJavaVersion(int recommendJavaVersion) { + this.recommendJavaVersion = recommendJavaVersion; + } + + public int getMinJavaVersion() { + return minJavaVersion; + } + + public void setMinJavaVersion(int minJavaVersion) { + this.minJavaVersion = minJavaVersion; + } + + public int getMaxJavaVersion() { + return maxJavaVersion; + } + + public void setMaxJavaVersion(int maxJavaVersion) { + this.maxJavaVersion = maxJavaVersion; + } + + public boolean isWarnMissJavaVersion() { + return warnMissJavaVersion; + } + + public void setWarnMissJavaVersion(boolean warnMissJavaVersion) { + this.warnMissJavaVersion = warnMissJavaVersion; + } + public void updateOptionalGraph() { for (OptionalFile file : updateOptional) { if (file.dependenciesFile != null) { @@ -376,6 +420,9 @@ public void verify() { for (String s : clientArgs) { if (s == null) throw new IllegalArgumentException("Found null entry in clientArgs"); } + for(String s : compatClasses) { + if (s == null) throw new IllegalArgumentException("Found null entry in compatClasses"); + } for (OptionalFile f : updateOptional) { if (f == null) throw new IllegalArgumentException("Found null entry in updateOptional"); if (f.name == null) throw new IllegalArgumentException("Optional: name must not be null"); @@ -418,6 +465,10 @@ public Map getProperties() { return Collections.unmodifiableMap(properties); } + public List getCompatClasses() { + return Collections.unmodifiableList(compatClasses); + } + public enum Version { MC125("1.2.5", 29), MC147("1.4.7", 51), @@ -486,6 +537,9 @@ public enum ClassLoaderConfig { AGENT, LAUNCHER } + public enum SignedClientConfig { + NONE, SIGNED + } @FunctionalInterface public interface pushOptionalClassPathCallback { diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/request/websockets/ClientJSONPoint.java b/LauncherAPI/src/main/java/pro/gravit/launcher/request/websockets/ClientJSONPoint.java index 8d64bc64..5c4d4974 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/request/websockets/ClientJSONPoint.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/request/websockets/ClientJSONPoint.java @@ -14,10 +14,18 @@ import io.netty.handler.codec.http.websocketx.WebSocketVersion; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; +import pro.gravit.launcher.CertificatePinningTrustManager; +import pro.gravit.launcher.LauncherInject; +import pro.gravit.launcher.LauncherNetworkAPI; import pro.gravit.utils.helper.LogHelper; import javax.net.ssl.SSLException; +import java.io.IOException; import java.net.URI; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; public abstract class ClientJSONPoint { @@ -29,6 +37,8 @@ public abstract class ClientJSONPoint { protected WebSocketClientHandler webSocketClientHandler; protected boolean ssl = false; protected int port; + @LauncherInject("launcher.certificatePinning") + private static boolean isCertificatePinning; public ClientJSONPoint(final String uri) throws SSLException { this(URI.create(uri)); @@ -49,7 +59,16 @@ public ClientJSONPoint(URI uri) throws SSLException { } else port = uri.getPort(); final SslContext sslCtx; if (ssl) { - sslCtx = SslContextBuilder.forClient().build(); + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + if(isCertificatePinning) { + try { + sslContextBuilder.trustManager(CertificatePinningTrustManager.getTrustManager()); + } catch (KeyStoreException | NoSuchAlgorithmException | IOException | CertificateException e) { + LogHelper.error(e); + sslContextBuilder.trustManager(); + } + } + sslCtx = sslContextBuilder.build(); } else sslCtx = null; bootstrap.group(group) .channel(NioSocketChannel.class) diff --git a/LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java b/LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java index 6e38d59b..12699f34 100644 --- a/LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java +++ b/LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java @@ -1,7 +1,11 @@ package pro.gravit.launcher; import pro.gravit.utils.helper.IOHelper; +import pro.gravit.utils.helper.LogHelper; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -11,6 +15,11 @@ import java.net.URL; import java.net.URLConnection; import java.nio.file.Path; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -20,6 +29,8 @@ public class AsyncDownloader { public static final Callback IGNORE = (ignored) -> { }; public final Callback callback; + @LauncherInject("launcher.certificatePinning") + private static boolean isCertificatePinning; public AsyncDownloader(Callback callback) { this.callback = callback; @@ -31,6 +42,14 @@ public AsyncDownloader() { public void downloadFile(URL url, Path target, long size) throws IOException { URLConnection connection = url.openConnection(); + if(isCertificatePinning) { + HttpsURLConnection connection1 = (HttpsURLConnection) connection; + try { + connection1.setSSLSocketFactory(makeSSLSocketFactory()); + } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | KeyManagementException e) { + throw new IOException(e); + } + } try (InputStream input = connection.getInputStream()) { transfer(input, target, size); } @@ -38,11 +57,25 @@ public void downloadFile(URL url, Path target, long size) throws IOException { public void downloadFile(URL url, Path target) throws IOException { URLConnection connection = url.openConnection(); + if(isCertificatePinning) { + HttpsURLConnection connection1 = (HttpsURLConnection) connection; + try { + connection1.setSSLSocketFactory(makeSSLSocketFactory()); + } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | KeyManagementException e) { + throw new IOException(e); + } + } try (InputStream input = connection.getInputStream()) { IOHelper.transfer(input, target); } } + public SSLSocketFactory makeSSLSocketFactory() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, KeyManagementException { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, CertificatePinningTrustManager.getTrustManager().getTrustManagers(), new SecureRandom()); + return sslContext.getSocketFactory(); + } + public void downloadListInOneThread(List files, String baseURL, Path targetDir) throws URISyntaxException, IOException { URI baseUri = new URI(baseURL); String scheme = baseUri.getScheme(); diff --git a/LauncherCore/src/main/java/pro/gravit/launcher/CertificatePinningTrustManager.java b/LauncherCore/src/main/java/pro/gravit/launcher/CertificatePinningTrustManager.java new file mode 100644 index 00000000..0c9838f7 --- /dev/null +++ b/LauncherCore/src/main/java/pro/gravit/launcher/CertificatePinningTrustManager.java @@ -0,0 +1,62 @@ +package pro.gravit.launcher; + +import pro.gravit.utils.helper.LogHelper; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; + +public final class CertificatePinningTrustManager { + @LauncherInject("launchercore.certificates") + private static List secureConfigCertificates; + private static X509Certificate[] certs = null; + private static X509Certificate[] getInternalCertificates() { + CertificateFactory certFactory = null; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + return new X509Certificate[0]; + } + CertificateFactory finalCertFactory = certFactory; + return secureConfigCertificates.stream().map((cert) -> { + try (InputStream input = new ByteArrayInputStream(cert)) { + return (X509Certificate) finalCertFactory.generateCertificate(input); + } catch (IOException | CertificateException e) { + LogHelper.error(e); + return null; + } + }).toArray(X509Certificate[]::new); + } + + public static X509Certificate[] getCertificates() { + if(certs == null) certs = getInternalCertificates(); + return Arrays.copyOf(certs, certs.length); + } + + public static TrustManagerFactory getTrustManager() throws KeyStoreException, NoSuchAlgorithmException, IOException, CertificateException { + if(certs == null) certs = getInternalCertificates(); + TrustManagerFactory factory = TrustManagerFactory.getInstance("X.509"); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(null, null); + + int i = 1; + for (X509Certificate cert: certs) { + String alias = Integer.toString(i); + keystore.setCertificateEntry(alias, cert); + i++; + } + factory.init(keystore); + return factory; + } +} diff --git a/LauncherCore/src/main/java/pro/gravit/utils/Version.java b/LauncherCore/src/main/java/pro/gravit/utils/Version.java index 49c4c07e..3e643fc5 100644 --- a/LauncherCore/src/main/java/pro/gravit/utils/Version.java +++ b/LauncherCore/src/main/java/pro/gravit/utils/Version.java @@ -6,7 +6,7 @@ public final class Version { public static final int MAJOR = 5; public static final int MINOR = 1; - public static final int PATCH = 9; + public static final int PATCH = 10; public static final int BUILD = 1; public static final Version.Type RELEASE = Type.DEV; public final int major;