Launcher/LaunchServer/src/main/java/pro/gravit/launchserver/command/service/SecurityCheckCommand.java

234 lines
11 KiB
Java

package pro.gravit.launchserver.command.service;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launchserver.LaunchServer;
import pro.gravit.launchserver.auth.protect.AdvancedProtectHandler;
import pro.gravit.launchserver.auth.protect.NoProtectHandler;
import pro.gravit.launchserver.auth.protect.StdProtectHandler;
import pro.gravit.launchserver.command.Command;
import pro.gravit.launchserver.components.ProGuardComponent;
import pro.gravit.launchserver.config.LaunchServerConfig;
import pro.gravit.launchserver.helper.SignHelper;
import pro.gravit.utils.helper.IOHelper;
import pro.gravit.utils.helper.JVMHelper;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
public class SecurityCheckCommand extends Command {
private static final Logger logger = LogManager.getLogger();
public SecurityCheckCommand(LaunchServer server) {
super(server);
}
public static void printCheckResult(String module, String comment, Boolean status) {
if (status == null) {
logger.warn("[%s] %s".formatted(module, comment));
} else if (status) {
logger.info("[%s] %s OK".formatted(module, comment));
} else {
logger.error("[%s] %s".formatted(module, comment));
}
}
@Override
public String getArgsDescription() {
return "[]";
}
@Override
public String getUsageDescription() {
return "check configuration";
}
@Override
public void invoke(String... args) {
LaunchServerConfig config = server.config;
config.auth.forEach((name, pair) -> {
});
if (config.protectHandler instanceof NoProtectHandler) {
printCheckResult("protectHandler", "protectHandler none", false);
} else if (config.protectHandler instanceof AdvancedProtectHandler) {
printCheckResult("protectHandler", "", true);
if (!((AdvancedProtectHandler) config.protectHandler).enableHardwareFeature) {
printCheckResult("protectHandler.hardwareId", "you can improve security by using hwid provider", null);
} else {
printCheckResult("protectHandler.hardwareId", "", true);
}
} else if (config.protectHandler instanceof StdProtectHandler) {
printCheckResult("protectHandler", "you can improve security by using advanced", null);
} else {
printCheckResult("protectHandler", "unknown protectHandler", null);
}
if (config.netty.address.startsWith("ws://")) {
if (config.netty.ipForwarding)
printCheckResult("netty.ipForwarding", "ipForwarding may be used to spoofing ip", null);
printCheckResult("netty.address", "websocket connection not secure", false);
} else if (config.netty.address.startsWith("wss://")) {
if (!config.netty.ipForwarding)
printCheckResult("netty.ipForwarding", "ipForwarding not enabled. authLimiter may be get incorrect ip", null);
printCheckResult("netty.address", "", true);
}
if (config.netty.launcherURL.startsWith("http://")) {
printCheckResult("netty.launcherUrl", "launcher jar download connection not secure", false);
} else if (config.netty.launcherURL.startsWith("https://")) {
printCheckResult("netty.launcherUrl", "", true);
}
if (config.netty.launcherEXEURL.startsWith("http://")) {
printCheckResult("netty.launcherExeUrl", "launcher exe download connection not secure", false);
} else if (config.netty.launcherEXEURL.startsWith("https://")) {
printCheckResult("netty.launcherExeUrl", "", true);
}
if (config.netty.downloadURL.startsWith("http://")) {
printCheckResult("netty.downloadUrl", "assets/clients download connection not secure", false);
} else if (config.netty.downloadURL.startsWith("https://")) {
printCheckResult("netty.downloadUrl", "", true);
}
if (!config.sign.enabled) {
printCheckResult("sign", "it is recommended to use a signature", null);
} else {
boolean bad = false;
try {
KeyStore keyStore = SignHelper.getStore(new File(config.sign.keyStore).toPath(), config.sign.keyStorePass, config.sign.keyStoreType);
Certificate[] certChainPlain = keyStore.getCertificateChain(config.sign.keyAlias);
List<X509Certificate> certChain = Arrays.stream(certChainPlain).map(e -> (X509Certificate) e).toList();
X509Certificate cert = certChain.get(0);
cert.checkValidity();
if (certChain.size() == 1) {
printCheckResult("sign", "certificate chain contains <2 element(recommend 2 and more)", false);
bad = true;
}
if ((cert.getBasicConstraints() & 1) == 1) {
printCheckResult("sign", "end certificate - CA", false);
bad = true;
}
for (X509Certificate certificate : certChain) {
certificate.checkValidity();
}
} catch (Throwable e) {
logger.error("Sign check failed", e);
bad = true;
}
if (!bad)
printCheckResult("sign", "", true);
}
if (config.components.values().stream().noneMatch(c -> c instanceof ProGuardComponent)) {
printCheckResult("launcher.enabledProGuard", "proguard not enabled", false);
} else {
printCheckResult("launcher.enabledProGuard", "", true);
}
if (!config.launcher.stripLineNumbers) {
printCheckResult("launcher.stripLineNumbers", "stripLineNumbers not enabled", false);
} else {
printCheckResult("launcher.stripLineNumbers", "", true);
}
switch (config.env) {
case DEV -> printCheckResult("env", "found env DEV", false);
case DEBUG -> printCheckResult("env", "found env DEBUG", false);
case STD -> printCheckResult("env", "you can improve security by using env PROD", null);
case PROD -> printCheckResult("env", "", true);
}
//Profiles
for (ClientProfile profile : server.getProfiles()) {
boolean bad = false;
String profileModuleName = "profiles.%s".formatted(profile.getTitle());
for (String exc : profile.getUpdateExclusions()) {
StringTokenizer tokenizer = new StringTokenizer(exc, "/");
if (exc.endsWith(".jar")) {
printCheckResult(profileModuleName, "updateExclusions %s not safe. Cheats may be injected very easy!".formatted(exc), false);
bad = true;
continue;
}
if (tokenizer.hasMoreTokens() && tokenizer.nextToken().equals("mods")) {
String nextToken = tokenizer.nextToken();
if (!tokenizer.hasMoreTokens()) {
if (!exc.endsWith("/")) {
printCheckResult(profileModuleName, "updateExclusions %s not safe. Cheats may be injected very easy!".formatted(exc), false);
bad = true;
}
} else {
if (nextToken.equals("memory_repo") || nextToken.equals(profile.getVersion().toString())) {
printCheckResult(profileModuleName, "updateExclusions %s not safe. Cheats may be injected very easy!".formatted(exc), false);
bad = true;
}
}
}
}
if (!bad)
printCheckResult(profileModuleName, "", true);
}
//Linux permissions check
if (JVMHelper.OS_TYPE == JVMHelper.OS.LINUX) {
try {
int uid = 0, gid = 0;
String[] status = new String(IOHelper.read(Paths.get("/proc/self/status"))).split("\n");
for (String line : status) {
String[] parts = line.split(":");
if (parts.length == 0) continue;
if (parts[0].trim().equalsIgnoreCase("Uid")) {
String[] words = parts[1].trim().split(" ");
uid = Integer.parseInt(words[0]);
if (Integer.parseInt(words[0]) == 0 || Integer.parseInt(words[0]) == 0) {
logger.error("The process is started as root! It is not recommended");
}
}
if (parts[0].trim().equalsIgnoreCase("Gid")) {
String[] words = parts[1].trim().split(" ");
gid = Integer.parseInt(words[0]);
if (Integer.parseInt(words[0]) == 0 || Integer.parseInt(words[0]) == 0) {
logger.error("The process is started as root group! It is not recommended");
}
}
}
if (checkOtherWriteAccess(IOHelper.getCodeSource(LaunchServer.class))) {
logger.warn("Write access to LaunchServer.jar. Please use 'chmod 755 LaunchServer.jar'");
}
if (Files.exists(server.dir.resolve(".keys")) && checkOtherReadOrWriteAccess(server.dir.resolve(".keys"))) {
logger.warn("Write or read access to .keys directory. Please use 'chmod -R 600 .keys'");
}
if (Files.exists(server.dir.resolve("LaunchServerConfig.json")) && checkOtherReadOrWriteAccess(server.dir.resolve("LaunchServerConfig.json"))) {
logger.warn("Write or read access to LaunchServerConfig.json. Please use 'chmod 600 LaunchServerConfig.json'");
}
if (Files.exists(server.dir.resolve("LaunchServerRuntimeConfig.json")) && checkOtherReadOrWriteAccess(server.dir.resolve("LaunchServerRuntimeConfig.json"))) {
logger.warn("Write or read access to LaunchServerRuntimeConfig.json. Please use 'chmod 600 LaunchServerRuntimeConfig.json'");
}
} catch (IOException e) {
logger.error(e);
}
}
logger.info("Check completed");
}
public boolean checkOtherWriteAccess(Path file) throws IOException {
Set<PosixFilePermission> permissionSet = Files.getPosixFilePermissions(file);
return permissionSet.contains(PosixFilePermission.OTHERS_WRITE);
}
public boolean checkOtherReadOrWriteAccess(Path file) throws IOException {
Set<PosixFilePermission> permissionSet = Files.getPosixFilePermissions(file);
return permissionSet.contains(PosixFilePermission.OTHERS_WRITE) || permissionSet.contains(PosixFilePermission.OTHERS_READ);
}
}