Proof of concept, now to figure out how it works!
This commit is contained in:
32
SentinelAuth/build.gradle
Normal file
32
SentinelAuth/build.gradle
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot' version '3.4.4'
|
||||||
|
id 'io.spring.dependency-management' version '1.1.7'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'me.trouper.sentinel'
|
||||||
|
version = '0.0.1-SNAPSHOT'
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('test') {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
bootJar {
|
||||||
|
mainClass = 'me.trouper.sentinel.auth.SentinelAuth' // Replace with your actual main class
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package me.trouper.sentinel.auth;
|
||||||
|
|
||||||
|
import me.trouper.sentinel.auth.data.AuthRequest;
|
||||||
|
import me.trouper.sentinel.auth.data.AuthResponse;
|
||||||
|
import me.trouper.sentinel.auth.services.JarService;
|
||||||
|
import me.trouper.sentinel.auth.services.LicenseService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class AuthController {
|
||||||
|
private final LicenseService licenseService;
|
||||||
|
private final JarService jarService;
|
||||||
|
|
||||||
|
public AuthController(LicenseService ls, JarService js) {
|
||||||
|
this.licenseService = ls;
|
||||||
|
this.jarService = js;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/authenticate")
|
||||||
|
public ResponseEntity<Map<String, String>> authenticate(@RequestBody AuthRequest request) {
|
||||||
|
for (String key : licenseService.getLicenseKeys()) {
|
||||||
|
String serverCode = LicenseService.generateAuthCode(key);
|
||||||
|
if (serverCode.equals(request.authCode())) {
|
||||||
|
Map<String, String> response = new HashMap<>();
|
||||||
|
response.put("decryptionKey", jarService.getDecryptionKey());
|
||||||
|
|
||||||
|
if (!jarService.getJarHash().equals(request.clientHash())) {
|
||||||
|
response.put("jarData",
|
||||||
|
Base64.getEncoder().encodeToString(jarService.getEncryptedJar()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package me.trouper.sentinel.auth;
|
||||||
|
|
||||||
|
import me.trouper.sentinel.auth.services.JarService;
|
||||||
|
import me.trouper.sentinel.auth.services.LicenseService;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class SentinelAuth {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(SentinelAuth.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.trouper.sentinel.auth.data;
|
||||||
|
|
||||||
|
public record AuthRequest(String authCode, String clientHash) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.trouper.sentinel.auth.data;
|
||||||
|
|
||||||
|
public record AuthResponse(byte[] jarData, String decryptionKey) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package me.trouper.sentinel.auth.services;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import me.trouper.sentinel.auth.utils.DigestUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class JarService {
|
||||||
|
private byte[] unencryptedJar;
|
||||||
|
private byte[] encryptedJar; // Store encrypted JAR
|
||||||
|
private String jarHash;
|
||||||
|
private String decryptionKey; // Will be initialized
|
||||||
|
private final Path unencryptedJarPath = Path.of("storage/SentinelPlugin-1.0-SNAPSHOT.jar");
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() throws IOException, GeneralSecurityException {
|
||||||
|
if (Files.exists(unencryptedJarPath)) {
|
||||||
|
this.unencryptedJar = Files.readAllBytes(unencryptedJarPath);
|
||||||
|
|
||||||
|
// Set encryption key and decrypt key
|
||||||
|
String encryptionKey = "AES_KEY_12345678";
|
||||||
|
this.decryptionKey = encryptionKey; // Initialize decryptionKey
|
||||||
|
|
||||||
|
// Encrypt once during startup
|
||||||
|
this.encryptedJar = encryptJar(unencryptedJar, encryptionKey);
|
||||||
|
this.jarHash = DigestUtils.getSHA256(encryptedJar);
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundException("Unencrypted plugin JAR not found at: " + unencryptedJarPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncryptedJar() {
|
||||||
|
return encryptedJar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] encryptJar(byte[] input, String encryptionKey) throws GeneralSecurityException {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
|
||||||
|
return cipher.doFinal(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJarHash() { return jarHash; }
|
||||||
|
public String getDecryptionKey() { return decryptionKey; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package me.trouper.sentinel.auth.services;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LicenseService {
|
||||||
|
private final List<String> licenseKeys = List.of("SECRET_KEY_12345"); // In real use, load from secure config
|
||||||
|
|
||||||
|
public List<String> getLicenseKeys() {
|
||||||
|
return licenseKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,29 @@
|
|||||||
|
package me.trouper.sentinel.auth.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(byte[] bytes) {
|
||||||
|
MessageDigest md = null;
|
||||||
|
try {
|
||||||
|
md = MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
byte[] digestBytes = md.digest(bytes);
|
||||||
|
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
SentinelAuth/src/main/resources/application.properties
Normal file
1
SentinelAuth/src/main/resources/application.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
spring.application.name=SentinelAuth
|
||||||
49
SentinelPlugin/build.gradle
Normal file
49
SentinelPlugin/build.gradle
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'me.trouper.sentinel'
|
||||||
|
version = '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
name = "papermc-repo"
|
||||||
|
url = "https://repo.papermc.io/repository/maven-public/"
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "sonatype"
|
||||||
|
url = "https://oss.sonatype.org/content/groups/public/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
|
||||||
|
}
|
||||||
|
|
||||||
|
def targetJavaVersion = 21
|
||||||
|
java {
|
||||||
|
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
|
||||||
|
sourceCompatibility = javaVersion
|
||||||
|
targetCompatibility = javaVersion
|
||||||
|
if (JavaVersion.current() < javaVersion) {
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
options.encoding = 'UTF-8'
|
||||||
|
|
||||||
|
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
|
||||||
|
options.release.set(targetJavaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
def props = [version: version]
|
||||||
|
inputs.properties props
|
||||||
|
filteringCharset 'UTF-8'
|
||||||
|
filesMatching('paper-plugin.yml') {
|
||||||
|
expand props
|
||||||
|
}
|
||||||
|
}
|
||||||
0
SentinelPlugin/gradle.properties
Normal file
0
SentinelPlugin/gradle.properties
Normal file
1
SentinelPlugin/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
1
SentinelPlugin/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
1
SentinelPlugin/settings.gradle
Normal file
1
SentinelPlugin/settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'SentinelPlugin'
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package me.trouper.sentinel.plugin;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandMap;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.PluginCommand;
|
||||||
|
import org.bukkit.command.defaults.BukkitCommand;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
public final class SentinelPlugin extends JavaPlugin {
|
||||||
|
|
||||||
|
public static void initialize(JavaPlugin loaderPlugin) {
|
||||||
|
loaderPlugin.getLogger().info("Hello from dynamically loaded Plugin!");
|
||||||
|
Bukkit.getScheduler().runTask(loaderPlugin, () -> {
|
||||||
|
PluginCommand command = Bukkit.getPluginCommand("demo");
|
||||||
|
if (command != null) {
|
||||||
|
command.setExecutor((sender, cmd, label, args) -> {
|
||||||
|
sender.sendMessage("Hello from dynamically loaded plugin!");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ plugins {
|
|||||||
id 'java'
|
id 'java'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'me.trouper'
|
group = 'me.trouper.sentinel'
|
||||||
version = '1.0-SNAPSHOT'
|
version = '1.0-SNAPSHOT'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
rootProject.name = 'SentinelLoader'
|
rootProject.name = 'SentinelLoader'
|
||||||
|
include 'SentinelAuth'
|
||||||
|
include 'SentinelPlugin'
|
||||||
|
|||||||
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
|
name: SentinelLoader
|
||||||
version: '1.0-SNAPSHOT'
|
version: '1.0-SNAPSHOT'
|
||||||
main: me.trouper.sentinelLoader.SentinelLoader
|
main: me.trouper.sentinel.loader.SentinelLoader
|
||||||
api-version: '1.21'
|
api-version: '1.21'
|
||||||
prefix: SentinelLoader
|
prefix: SentinelLoader
|
||||||
load: STARTUP
|
load: STARTUP
|
||||||
|
|||||||
Reference in New Issue
Block a user