Proof of concept, now to figure out how it works!
This commit is contained in:
221
src/main/java/me/trouper/sentinel/loader/SentinelLoader.java
Normal file
221
src/main/java/me/trouper/sentinel/loader/SentinelLoader.java
Normal file
@@ -0,0 +1,221 @@
|
||||
package me.trouper.sentinel.loader;
|
||||
|
||||
import me.trouper.sentinel.loader.data.AuthResponse;
|
||||
import me.trouper.sentinel.loader.utils.AuthUtils;
|
||||
import me.trouper.sentinel.loader.utils.DigestUtils;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class SentinelLoader extends JavaPlugin {
|
||||
|
||||
private String licenseKey = "SECRET_KEY_12345";
|
||||
private Path encryptedJarPath;
|
||||
private static SentinelLoader instance;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
|
||||
try {
|
||||
// Ensure data folder exists
|
||||
if (!getDataFolder().exists() && !getDataFolder().mkdirs()) {
|
||||
getLogger().severe("Failed to create plugin data folder!");
|
||||
return;
|
||||
}
|
||||
|
||||
encryptedJarPath = getDataFolder().toPath().resolve("encrypted-plugin.jar");
|
||||
|
||||
// Get client hash safely
|
||||
String clientHash = "";
|
||||
if (Files.exists(encryptedJarPath)) {
|
||||
try {
|
||||
clientHash = DigestUtils.getSHA256(encryptedJarPath);
|
||||
} catch (IOException e) {
|
||||
getLogger().log(Level.WARNING, "Corrupted JAR file detected, will re-download", e);
|
||||
try {
|
||||
Files.deleteIfExists(encryptedJarPath);
|
||||
} catch (IOException ex) {
|
||||
getLogger().log(Level.SEVERE, "Failed to delete corrupted JAR file", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String authCode = AuthUtils.generateAuthCode(licenseKey);
|
||||
getLogger().info("Sending auth Auth Code: " + authCode);
|
||||
AuthResponse response = sendAuthRequest(authCode, clientHash);
|
||||
|
||||
if (response == null) {
|
||||
getLogger().severe("Authentication failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle JAR download
|
||||
if (response.jarData() != null) {
|
||||
try {
|
||||
Files.createDirectories(encryptedJarPath.getParent());
|
||||
Files.write(encryptedJarPath, response.jarData());
|
||||
getLogger().info("Successfully updated plugin JAR");
|
||||
} catch (IOException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to save encrypted JAR", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the plugin
|
||||
try {
|
||||
byte[] decryptedJar = decrypt(
|
||||
Files.readAllBytes(encryptedJarPath),
|
||||
response.decryptionKey()
|
||||
);
|
||||
getLogger().info("Trying to load plugin...");
|
||||
loadPlugin(decryptedJar);
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to decrypt or load plugin", e);
|
||||
try {
|
||||
Files.deleteIfExists(encryptedJarPath);
|
||||
} catch (IOException ex) {
|
||||
getLogger().log(Level.SEVERE, "Failed to cleanup invalid JAR", ex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().log(Level.SEVERE, "Critical error during initialization", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private AuthResponse sendAuthRequest(String authCode, String clientHash) {
|
||||
try {
|
||||
URL url = new URL("http://localhost:8080/authenticate");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
|
||||
// Write request body
|
||||
String requestBody = String.format(
|
||||
"{\"authCode\":\"%s\",\"clientHash\":\"%s\"}",
|
||||
authCode,
|
||||
clientHash
|
||||
);
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input = requestBody.getBytes(StandardCharsets.UTF_8);
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
|
||||
// Read response
|
||||
if (conn.getResponseCode() != 200) {
|
||||
SentinelLoader.getInstance().getLogger().warning("Auth server responded with code: " + conn.getResponseCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
try (BufferedReader br = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
StringBuilder response = new StringBuilder();
|
||||
String responseLine;
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
|
||||
// Simple JSON parsing (for production use a proper library like Gson)
|
||||
String json = response.toString();
|
||||
boolean hasJar = json.contains("\"jarData\"");
|
||||
String decryptionKey = extractJsonValue(json, "decryptionKey");
|
||||
|
||||
if (hasJar) {
|
||||
String jarDataBase64 = extractJsonValue(json, "jarData");
|
||||
byte[] jarData = Base64.getDecoder().decode(jarDataBase64);
|
||||
return new AuthResponse(jarData, decryptionKey);
|
||||
} else {
|
||||
return new AuthResponse(null, decryptionKey);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
SentinelLoader.getInstance().getLogger().log(Level.SEVERE, "Failed to authenticate with server", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to extract values from simple JSON
|
||||
private String extractJsonValue(String json, String key) {
|
||||
int start = json.indexOf("\"" + key + "\":") + key.length() + 3;
|
||||
int end = json.indexOf("\"", start + 1);
|
||||
if (end == -1) end = json.indexOf(",", start);
|
||||
if (end == -1) end = json.indexOf("}", start);
|
||||
return json.substring(start, end).replace("\"", "");
|
||||
}
|
||||
|
||||
private byte[] decrypt(byte[] data, String key) throws GeneralSecurityException {
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(), "AES"));
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private void loadPlugin(byte[] decryptedJar) throws Exception {
|
||||
new InMemoryClassLoader(getClassLoader(), decryptedJar)
|
||||
.loadClass("me.trouper.sentinel.plugin.SentinelPlugin")
|
||||
.getMethod("initialize", JavaPlugin.class)
|
||||
.invoke(null, this);
|
||||
}
|
||||
|
||||
private static class InMemoryClassLoader extends ClassLoader {
|
||||
private final Map<String, Class<?>> classes = new HashMap<>();
|
||||
private final byte[] jarBytes;
|
||||
|
||||
public InMemoryClassLoader(ClassLoader parent, byte[] jarBytes) {
|
||||
super(parent);
|
||||
this.jarBytes = jarBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
if (classes.containsKey(name)) return classes.get(name);
|
||||
|
||||
try (JarInputStream jar = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
|
||||
JarEntry entry;
|
||||
while ((entry = jar.getNextJarEntry()) != null) {
|
||||
if (!entry.getName().endsWith(".class")) continue;
|
||||
|
||||
String className = entry.getName()
|
||||
.replace(".class", "")
|
||||
.replace('/', '.');
|
||||
|
||||
if (!className.equals(name)) continue;
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = jar.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, len);
|
||||
}
|
||||
|
||||
byte[] bytes = bos.toByteArray();
|
||||
Class<?> clazz = defineClass(className, bytes, 0, bytes.length);
|
||||
classes.put(className, clazz);
|
||||
return clazz;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ClassNotFoundException("Class not found: " + name, e);
|
||||
}
|
||||
throw new ClassNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
public static SentinelLoader getInstance() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package me.trouper.sentinel.loader.data;
|
||||
|
||||
public record AuthRequest(String authCode, String clientHash) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package me.trouper.sentinel.loader.data;
|
||||
|
||||
public record AuthResponse(byte[] jarData, String decryptionKey) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package me.trouper.sentinel.loader.utils;
|
||||
|
||||
import me.trouper.sentinel.loader.SentinelLoader;
|
||||
import me.trouper.sentinel.loader.data.AuthResponse;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class AuthUtils {
|
||||
|
||||
|
||||
public static String generateAuthCode(String licenseKey) {
|
||||
try {
|
||||
byte[] key = licenseKey.getBytes(StandardCharsets.UTF_8);
|
||||
long counter = Instant.now().getEpochSecond() / 10;
|
||||
byte[] counterBytes = ByteBuffer.allocate(8).putLong(counter).array();
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA1"));
|
||||
byte[] hmac = mac.doFinal(counterBytes);
|
||||
|
||||
int offset = hmac[hmac.length - 1] & 0xF;
|
||||
int binary = ((hmac[offset] & 0x7F) << 24)
|
||||
| ((hmac[offset + 1] & 0xFF) << 16)
|
||||
| ((hmac[offset + 2] & 0xFF) << 8)
|
||||
| (hmac[offset + 3] & 0xFF);
|
||||
|
||||
return String.format("%06d", binary % 1_000_000);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package me.trouper.sentinel.loader.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class DigestUtils {
|
||||
public static String getSHA256(Path filePath) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] fileBytes = Files.readAllBytes(filePath);
|
||||
byte[] digestBytes = md.digest(fileBytes);
|
||||
return bytesToHex(digestBytes);
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
String hex = String.format("%02x", b);
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package me.trouper.sentinelLoader;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class SentinelLoader extends JavaPlugin {
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Plugin startup logic
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Plugin shutdown logic
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
name: SentinelLoader
|
||||
version: '1.0-SNAPSHOT'
|
||||
main: me.trouper.sentinelLoader.SentinelLoader
|
||||
main: me.trouper.sentinel.loader.SentinelLoader
|
||||
api-version: '1.21'
|
||||
prefix: SentinelLoader
|
||||
load: STARTUP
|
||||
|
||||
Reference in New Issue
Block a user