Proof of concept, now to figure out how it works!

This commit is contained in:
thetrouper
2025-03-25 06:15:10 -05:00
parent 8a755bd642
commit 96c758edd0
23 changed files with 608 additions and 18 deletions

View 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;
}
}

View File

@@ -0,0 +1,5 @@
package me.trouper.sentinel.loader.data;
public record AuthRequest(String authCode, String clientHash) {
}

View File

@@ -0,0 +1,5 @@
package me.trouper.sentinel.loader.data;
public record AuthResponse(byte[] jarData, String decryptionKey) {
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}

View File

@@ -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