[FEATURE] Support encrypted runtime

This commit is contained in:
Gravita 2021-04-26 22:21:28 +07:00
parent 203fc638dc
commit 68d7c0a947
11 changed files with 180 additions and 22 deletions

View file

@ -8,6 +8,10 @@
import pro.gravit.utils.helper.LogHelper; import pro.gravit.utils.helper.LogHelper;
import pro.gravit.utils.helper.SecurityHelper; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@ -16,6 +20,8 @@
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor; import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -70,6 +76,10 @@ public void pushDir(Path dir, String targetDir, Map<String, byte[]> hashMap, boo
IOHelper.walk(dir, new RuntimeDirVisitor(output, hashMap, dir, targetDir), hidden); IOHelper.walk(dir, new RuntimeDirVisitor(output, hashMap, dir, targetDir), hidden);
} }
public void pushEncryptedDir(Path dir, String targetDir, String aesHexKey, Map<String, byte[]> 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 { public void pushBytes(String filename, byte[] bytes) throws IOException {
ZipEntry zip = IOHelper.newZipEntry(filename); ZipEntry zip = IOHelper.newZipEntry(filename);
output.putNextEntry(zip); output.putNextEntry(zip);
@ -178,4 +188,55 @@ private ZipEntry newEntry(String fileName) {
return newZipEntry(targetDir + IOHelper.CROSS_SEPARATOR + fileName); return newZipEntry(targetDir + IOHelper.CROSS_SEPARATOR + fileName);
} }
} }
private final static class EncryptedRuntimeDirVisitor extends SimpleFileVisitor<Path> {
private final ZipOutputStream output;
private final Map<String, byte[]> hashs;
private final Path sourceDir;
private final String targetDir;
private final SecretKeySpec sKeySpec;
private EncryptedRuntimeDirVisitor(ZipOutputStream output, String aesKey, Map<String, byte[]> 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);
}
}
} }

View file

@ -69,10 +69,14 @@ public Path process(Path inputJar) throws IOException {
// map for guard // map for guard
Map<String, byte[]> runtime = new HashMap<>(256); Map<String, byte[]> runtime = new HashMap<>(256);
// Write launcher guard dir // Write launcher guard dir
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.runtimeDir, Launcher.RUNTIME_DIR, runtime, false);
}
context.pushDir(server.launcherBinary.guardDir, Launcher.GUARD_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); context.pushFile(Launcher.CONFIG_FILE, launcherConfig);
postBuildHook.hook(context); postBuildHook.hook(context);
} }
@ -109,6 +113,8 @@ protected void initProps() {
properties.put("launcher.guardType", server.config.launcher.guardType); properties.put("launcher.guardType", server.config.launcher.guardType);
properties.put("launchercore.env", server.config.env); properties.put("launchercore.env", server.config.env);
properties.put("launcher.memory", server.config.launcher.memoryLimit); 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("launcher.certificatePinning", server.config.launcher.certificatePinning);
properties.put("runtimeconfig.passwordEncryptKey", server.runtime.passwordEncryptKey); properties.put("runtimeconfig.passwordEncryptKey", server.runtime.passwordEncryptKey);
String launcherSalt = SecurityHelper.randomStringToken(); String launcherSalt = SecurityHelper.randomStringToken();

View file

@ -299,6 +299,7 @@ public static class LauncherConf {
public boolean stripLineNumbers; public boolean stripLineNumbers;
public boolean deleteTempFiles; public boolean deleteTempFiles;
public boolean certificatePinning; public boolean certificatePinning;
public boolean encryptRuntime;
public int memoryLimit = 256; public int memoryLimit = 256;
} }

View file

@ -5,6 +5,7 @@
public class LaunchServerRuntimeConfig { public class LaunchServerRuntimeConfig {
public String passwordEncryptKey; public String passwordEncryptKey;
public String runtimeEncryptKey;
public String oemUnlockKey; public String oemUnlockKey;
public String registerApiKey; public String registerApiKey;
public String clientCheckSecret; public String clientCheckSecret;
@ -19,6 +20,7 @@ public void verify() {
public void reset() { public void reset() {
passwordEncryptKey = SecurityHelper.randomStringToken(); passwordEncryptKey = SecurityHelper.randomStringToken();
runtimeEncryptKey = SecurityHelper.randomStringAESKey();
registerApiKey = SecurityHelper.randomStringToken(); registerApiKey = SecurityHelper.randomStringToken();
clientCheckSecret = SecurityHelper.randomStringToken(); clientCheckSecret = SecurityHelper.randomStringToken();
} }

View file

@ -14,6 +14,7 @@
import java.lang.invoke.MethodType; import java.lang.invoke.MethodType;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.interfaces.ECPublicKey; import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.util.*; import java.util.*;
@ -28,7 +29,10 @@ public final class LauncherConfig extends StreamObject {
@LauncherInject("launcher.port") @LauncherInject("launcher.port")
public final int clientPort; public final int clientPort;
public final LauncherTrustManager trustManager; public final LauncherTrustManager trustManager;
public final ECPublicKey publicKey; @Deprecated
public ECPublicKey publicKey = null;
public final ECPublicKey ecdsaPublicKey;
public final RSAPublicKey rsaPublicKey;
public final Map<String, byte[]> runtime; public final Map<String, byte[]> runtime;
@Deprecated @Deprecated
public final boolean isWarningMissArchJava; public final boolean isWarningMissArchJava;
@ -40,8 +44,10 @@ public final class LauncherConfig extends StreamObject {
public final String secureCheckSalt; public final String secureCheckSalt;
@LauncherInject("runtimeconfig.passwordEncryptKey") @LauncherInject("runtimeconfig.passwordEncryptKey")
public final String passwordEncryptKey; public final String passwordEncryptKey;
@LauncherInject("runtimeconfig.runtimeEncryptKey")
public final String runtimeEncryptKey;
@LauncherInject("launcher.address") @LauncherInject("launcher.address")
public String address; public final String address;
@LauncherInject("runtimeconfig.secretKeyClient") @LauncherInject("runtimeconfig.secretKeyClient")
public String secretKeyClient; public String secretKeyClient;
@LauncherInject("runtimeconfig.oemUnlockKey") @LauncherInject("runtimeconfig.oemUnlockKey")
@ -52,10 +58,13 @@ public final class LauncherConfig extends StreamObject {
@LauncherInjectionConstructor @LauncherInjectionConstructor
public LauncherConfig(HInput input) throws IOException, InvalidKeySpecException { 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; secureCheckHash = null;
secureCheckSalt = null; secureCheckSalt = null;
passwordEncryptKey = null; passwordEncryptKey = null;
runtimeEncryptKey = null;
projectName = null; projectName = null;
clientPort = -1; clientPort = -1;
secretKeyClient = null; secretKeyClient = null;
@ -83,9 +92,12 @@ public LauncherConfig(HInput input) throws IOException, InvalidKeySpecException
runtime = Collections.unmodifiableMap(localResources); runtime = Collections.unmodifiableMap(localResources);
} }
@Deprecated
public LauncherConfig(String address, ECPublicKey publicKey, Map<String, byte[]> runtime, String projectName) { public LauncherConfig(String address, ECPublicKey publicKey, Map<String, byte[]> runtime, String projectName) {
this.address = address; this.address = address;
this.publicKey = publicKey; this.publicKey = publicKey;
this.ecdsaPublicKey = this.publicKey;
this.rsaPublicKey = null;
this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime)); this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime));
this.projectName = projectName; this.projectName = projectName;
this.clientPort = 32148; this.clientPort = 32148;
@ -95,6 +107,24 @@ public LauncherConfig(String address, ECPublicKey publicKey, Map<String, byte[]>
secureCheckSalt = null; secureCheckSalt = null;
secureCheckHash = null; secureCheckHash = null;
passwordEncryptKey = null; passwordEncryptKey = null;
runtimeEncryptKey = null;
trustManager = null;
}
public LauncherConfig(String address, ECPublicKey ecdsaPublicKey, RSAPublicKey rsaPublicKey, Map<String, byte[]> 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; trustManager = null;
} }
@ -103,14 +133,16 @@ public LauncherConfig(String address, Map<String, byte[]> runtime, String projec
this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime)); this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime));
this.projectName = projectName; this.projectName = projectName;
this.clientPort = 32148; this.clientPort = 32148;
this.publicKey = null;
this.trustManager = trustManager; this.trustManager = trustManager;
this.rsaPublicKey = null;
this.ecdsaPublicKey = null;
environment = env; environment = env;
guardType = "no"; guardType = "no";
isWarningMissArchJava = true; isWarningMissArchJava = true;
secureCheckSalt = null; secureCheckSalt = null;
secureCheckHash = null; secureCheckHash = null;
passwordEncryptKey = null; passwordEncryptKey = null;
runtimeEncryptKey = null;
} }
public static void initModules(LauncherModulesManager modulesManager) { public static void initModules(LauncherModulesManager modulesManager) {
@ -126,7 +158,8 @@ public static void initModules(LauncherModulesManager modulesManager) {
@Override @Override
public void write(HOutput output) throws IOException { 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 // Write signed runtime
Set<Map.Entry<String, byte[]>> entrySet = runtime.entrySet(); Set<Map.Entry<String, byte[]>> entrySet = runtime.entrySet();

View file

@ -5,6 +5,7 @@
public class OptionalDepend { public class OptionalDepend {
@LauncherNetworkAPI @LauncherNetworkAPI
public String name; public String name;
@Deprecated
@LauncherNetworkAPI @LauncherNetworkAPI
public OptionalType type; public OptionalType type;
} }

View file

@ -49,8 +49,10 @@ public class OptionalFile {
public boolean isPreset; public boolean isPreset;
@Deprecated @Deprecated
public transient Set<OptionalFile> dependenciesCount; public transient Set<OptionalFile> dependenciesCount;
@Deprecated
private volatile transient Collection<BiConsumer<OptionalFile, Boolean>> watchList = null; private volatile transient Collection<BiConsumer<OptionalFile, Boolean>> watchList = null;
@Deprecated
public static OptionalType readType(HInput input) throws IOException { public static OptionalType readType(HInput input) throws IOException {
int t = input.readInt(); int t = input.readInt();
OptionalType type; OptionalType type;
@ -87,6 +89,7 @@ public int hashCode() {
return Objects.hash(name); return Objects.hash(name);
} }
@Deprecated
public OptionalType getType() { public OptionalType getType() {
return OptionalType.FILE; return OptionalType.FILE;
} }
@ -128,21 +131,25 @@ public void writeType(HOutput output) throws IOException {
} }
} }
@Deprecated
public void registerWatcher(BiConsumer<OptionalFile, Boolean> watcher) { public void registerWatcher(BiConsumer<OptionalFile, Boolean> watcher) {
if (watchList == null) watchList = ConcurrentHashMap.newKeySet(); if (watchList == null) watchList = ConcurrentHashMap.newKeySet();
watchList.add(watcher); watchList.add(watcher);
} }
@Deprecated
public void removeWatcher(BiConsumer<OptionalFile, Boolean> watcher) { public void removeWatcher(BiConsumer<OptionalFile, Boolean> watcher) {
if (watchList == null) return; if (watchList == null) return;
watchList.remove(watcher); watchList.remove(watcher);
} }
@Deprecated
public void clearAllWatchers() { public void clearAllWatchers() {
if (watchList == null) return; if (watchList == null) return;
watchList.clear(); watchList.clear();
} }
@Deprecated
public void watchEvent(boolean isMark) { public void watchEvent(boolean isMark) {
if (watchList == null) return; if (watchList == null) return;
watchList.forEach((e) -> e.accept(this, isMark)); watchList.forEach((e) -> e.accept(this, isMark));

View file

@ -2,6 +2,7 @@
import pro.gravit.launcher.LauncherNetworkAPI; import pro.gravit.launcher.LauncherNetworkAPI;
@Deprecated
public enum OptionalType { public enum OptionalType {
@LauncherNetworkAPI @LauncherNetworkAPI
FILE, FILE,

View file

@ -7,11 +7,17 @@
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer;
public class OptionalView { public class OptionalView {
public Set<OptionalFile> enabled = new HashSet<>(); public Set<OptionalFile> enabled = new HashSet<>();
@Deprecated
public Map<OptionalFile, Set<OptionalFile>> dependenciesCountMap = new HashMap<>(); public Map<OptionalFile, Set<OptionalFile>> dependenciesCountMap = new HashMap<>();
public Map<OptionalFile, OptionalFileInstallInfo> installInfo = new HashMap<>();
public Set<OptionalFile> all; public Set<OptionalFile> all;
public static class OptionalFileInstallInfo {
public boolean isManual;
}
public OptionalView(ClientProfile profile) { public OptionalView(ClientProfile profile) {
this.all = profile.getOptional(); this.all = profile.getOptional();
@ -23,6 +29,7 @@ public OptionalView(ClientProfile profile) {
public OptionalView(OptionalView view) { public OptionalView(OptionalView view) {
this.enabled = new HashSet<>(view.enabled); this.enabled = new HashSet<>(view.enabled);
this.dependenciesCountMap = new HashMap<>(view.dependenciesCountMap); this.dependenciesCountMap = new HashMap<>(view.dependenciesCountMap);
this.installInfo = new HashMap<>(view.installInfo);
this.all = view.all; this.all = view.all;
} }
@ -62,6 +69,7 @@ public Set<OptionalAction> getDisabledActions() {
return results; return results;
} }
@Deprecated
public void enable(OptionalFile file) { public void enable(OptionalFile file) {
if (enabled.contains(file)) return; if (enabled.contains(file)) return;
enabled.add(file); enabled.add(file);
@ -80,6 +88,7 @@ public void enable(OptionalFile file) {
} }
} }
@Deprecated
public void disable(OptionalFile file) { public void disable(OptionalFile file) {
if (!enabled.remove(file)) return; if (!enabled.remove(file)) return;
file.watchEvent(false); file.watchEvent(false);
@ -104,4 +113,52 @@ public void disable(OptionalFile file) {
} }
} }
} }
public void enable(OptionalFile file, boolean manual, BiConsumer<OptionalFile, Boolean> 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<OptionalFile, Boolean> 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;
}
} }

View file

@ -1,7 +1,5 @@
package pro.gravit.utils.helper; package pro.gravit.utils.helper;
import org.bouncycastle.jcajce.provider.asymmetric.RSA;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
@ -489,7 +487,7 @@ public static Cipher newRSAEncryptCipher(RSAPublicKey publicKey) {
//AES //AES
public static byte[] encrypt(String seed, byte[] cleartext) throws Exception { public static byte[] encrypt(String seed, byte[] cleartext) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes()); byte[] rawKey = getAESKey(seed.getBytes());
return encrypt(rawKey, cleartext); return encrypt(rawKey, cleartext);
} }
@ -497,7 +495,7 @@ public static byte[] encrypt(String seed, String cleartext) throws Exception {
return encrypt(seed, cleartext.getBytes()); 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"); KeyGenerator kGen = KeyGenerator.getInstance("AES");
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG"); SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(seed); 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 { 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) { public static byte[] fromHex(String hexString) {

View file

@ -215,16 +215,7 @@ public void run(String... args) throws Throwable {
public void updateLauncherConfig() { public void updateLauncherConfig() {
LauncherConfig cfg = null; LauncherConfig cfg = new LauncherConfig(config.address, null, null, new HashMap<>(), config.projectname);
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);
}
Launcher.setConfig(cfg); Launcher.setConfig(cfg);
} }