mirror of
https://github.com/GravitLauncher/Launcher
synced 2024-12-22 16:41:46 +03:00
[FEATURE] Support encrypted runtime
This commit is contained in:
parent
203fc638dc
commit
68d7c0a947
11 changed files with 180 additions and 22 deletions
|
@ -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<String, byte[]> hashMap, boo
|
|||
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 {
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,10 +69,14 @@ public Path process(Path inputJar) throws IOException {
|
|||
// map for guard
|
||||
Map<String, byte[]> 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();
|
||||
|
|
|
@ -299,6 +299,7 @@ public static class LauncherConf {
|
|||
public boolean stripLineNumbers;
|
||||
public boolean deleteTempFiles;
|
||||
public boolean certificatePinning;
|
||||
public boolean encryptRuntime;
|
||||
public int memoryLimit = 256;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<String, byte[]> 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<String, byte[]> 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<String, byte[]>
|
|||
secureCheckSalt = null;
|
||||
secureCheckHash = 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;
|
||||
}
|
||||
|
||||
|
@ -103,14 +133,16 @@ public LauncherConfig(String address, Map<String, byte[]> 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<Map.Entry<String, byte[]>> entrySet = runtime.entrySet();
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
public class OptionalDepend {
|
||||
@LauncherNetworkAPI
|
||||
public String name;
|
||||
@Deprecated
|
||||
@LauncherNetworkAPI
|
||||
public OptionalType type;
|
||||
}
|
||||
|
|
|
@ -49,8 +49,10 @@ public class OptionalFile {
|
|||
public boolean isPreset;
|
||||
@Deprecated
|
||||
public transient Set<OptionalFile> dependenciesCount;
|
||||
@Deprecated
|
||||
private volatile transient Collection<BiConsumer<OptionalFile, Boolean>> 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<OptionalFile, Boolean> watcher) {
|
||||
if (watchList == null) watchList = ConcurrentHashMap.newKeySet();
|
||||
watchList.add(watcher);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void removeWatcher(BiConsumer<OptionalFile, Boolean> 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));
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import pro.gravit.launcher.LauncherNetworkAPI;
|
||||
|
||||
@Deprecated
|
||||
public enum OptionalType {
|
||||
@LauncherNetworkAPI
|
||||
FILE,
|
||||
|
|
|
@ -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<OptionalFile> enabled = new HashSet<>();
|
||||
@Deprecated
|
||||
public Map<OptionalFile, Set<OptionalFile>> dependenciesCountMap = new HashMap<>();
|
||||
public Map<OptionalFile, OptionalFileInstallInfo> installInfo = new HashMap<>();
|
||||
public Set<OptionalFile> 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<OptionalAction> 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<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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue