Initial Commit

This commit is contained in:
Melledy
2025-10-27 02:02:26 -07:00
commit f58951fe2a
378 changed files with 315914 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
package emu.nebula.util;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.asn1.nist.NISTNamedCurves;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.*;
// Official Name: AeadTool
public class AeadHelper {
private static final ThreadLocal<SecureRandom> random = new ThreadLocal<>() {
@Override
protected SecureRandom initialValue() {
return new SecureRandom();
}
};
public static final byte[] serverGarbleKey = "xNdVF^XTa6T3HCUATMQ@sKMLzAw&%L!3".getBytes(StandardCharsets.US_ASCII); // Global
public static final byte[] serverMetaKey = "ma5Dn2FhC*Xhxy%c".getBytes(StandardCharsets.US_ASCII); // Global
public static byte[] generateBytes(int size) {
byte[] iv = new byte[size];
random.get().nextBytes(iv);
return iv;
}
// AES CBC
public static byte[] encryptCBC(byte[] messageData) throws Exception {
byte[] iv = generateBytes(16);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(
Cipher.ENCRYPT_MODE,
new SecretKeySpec(serverMetaKey, "AES"),
new IvParameterSpec(iv)
);
byte[] encrypted = cipher.doFinal(messageData);
byte[] data = new byte[encrypted.length + iv.length];
System.arraycopy(iv, 0, data, 0, iv.length);
System.arraycopy(encrypted, 0, data, iv.length, encrypted.length);
return data;
}
public static byte[] decryptCBC(byte[] messageData) throws Exception {
byte[] iv = new byte[16];
byte[] data = new byte[messageData.length - iv.length];
System.arraycopy(messageData, 0, iv, 0, iv.length);
System.arraycopy(messageData, iv.length, data, 0, data.length);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(
Cipher.DECRYPT_MODE,
new SecretKeySpec(serverMetaKey, "AES"),
new IvParameterSpec(iv)
);
return cipher.doFinal(data);
}
// AES GCM
public static byte[] encryptGCM(byte[] messageData, byte[] key) throws Exception {
byte[] iv = generateBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(
Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, iv)
);
cipher.updateAAD(iv);
byte[] encrypted = cipher.doFinal(messageData);
byte[] data = new byte[encrypted.length + iv.length];
System.arraycopy(iv, 0, data, 0, iv.length);
System.arraycopy(encrypted, 0, data, iv.length, encrypted.length);
return data;
}
public static byte[] decryptGCM(byte[] messageData, byte[] key) throws Exception {
byte[] iv = new byte[12];
byte[] data = new byte[messageData.length - iv.length];
System.arraycopy(messageData, 0, iv, 0, iv.length);
System.arraycopy(messageData, iv.length, data, 0, data.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(
Cipher.DECRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, iv)
);
cipher.updateAAD(iv);
return cipher.doFinal(data);
}
// Chacha20
public static byte[] encryptChaCha(byte[] messageData, byte[] key) throws Exception {
byte[] iv = generateBytes(12);
Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding");
cipher.init(
Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, "ChaCha20"),
new IvParameterSpec(iv)
);
cipher.updateAAD(iv);
byte[] encrypted = cipher.doFinal(messageData);
byte[] data = new byte[encrypted.length + iv.length];
System.arraycopy(iv, 0, data, 0, iv.length);
System.arraycopy(encrypted, 0, data, iv.length, encrypted.length);
return data;
}
public static byte[] decryptChaCha(byte[] messageData, byte[] key) throws Exception {
byte[] iv = new byte[12];
byte[] data = new byte[messageData.length - iv.length];
System.arraycopy(messageData, 0, iv, 0, iv.length);
System.arraycopy(messageData, iv.length, data, 0, data.length);
Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding");
cipher.init(
Cipher.DECRYPT_MODE,
new SecretKeySpec(key, "ChaCha20"),
new IvParameterSpec(iv)
);
cipher.updateAAD(iv);
return cipher.doFinal(data);
}
// XOR
public static byte[] encryptBasic(byte[] messageData, byte[] key3) {
byte[] data = new byte[messageData.length];
System.arraycopy(messageData, 0, data, 0, data.length);
for (int i = 0; i < data.length; i++) {
data[i] ^= key3[i % key3.length];
int b = data[i];
byte v7 = (byte) (b << 1);
byte v1 = (byte) ((b >> 7) & 0b00000001);
data[i] = (byte) (v1 | v7);
data[i] ^= data.length;
}
return data;
}
public static byte[] decryptBasic(byte[] messageData, byte[] key3) {
byte[] data = new byte[messageData.length];
System.arraycopy(messageData, 0, data, 0, data.length);
for (int i = 0; i < data.length; i++) {
data[i] ^= data.length;
int b = data[i];
byte v1 = (byte) (b << 7);
byte v7 = (byte) ((b >> 1) & 0b01111111);
data[i] = (byte) (v7 | v1);
data[i] ^= key3[i % key3.length];
}
return data;
}
// ECDH
public static AsymmetricCipherKeyPair generateECDHKEyPair() {
var generator = new ECKeyPairGenerator();
var p = NISTNamedCurves.getByName("P-256");
var domainParams = new ECDomainParameters(p.getCurve(), p.getG(), p.getN(), p.getH());
var genParams = new ECKeyGenerationParameters(domainParams, random.get());
generator.init(genParams);
return generator.generateKeyPair();
}
public static byte[] generateKey(byte[] clientPublic, byte[] serverPublic, byte[] serverPrivate) {
// Setup
byte[] ikm = new byte[32];
byte[] salt = serverPublic;
byte[] info = new byte[serverPublic.length];
// Create info
for (int i = 0; i < info.length; i++) {
int c = clientPublic[i] & 0xff;
int s = serverPublic[i] & 0xff;
if (c > s) {
s = (s << 1);
} else {
s = ((s >> 1) & 0b01111111);
}
info[i] = (byte) (s ^ c);
}
var sharedKey = calcECDHSharedKey(clientPublic, serverPrivate);
int count = Math.min(sharedKey.length, 32);
System.arraycopy(sharedKey, 0, ikm, 32 - count, count);
// Generator
var generator = new HKDFBytesGenerator(new SHA256Digest());
var genParams = new HKDFParameters(ikm, salt, info);
byte[] output = new byte[32];
generator.init(genParams);
generator.generateBytes(output, 0, output.length);
return output;
}
public static byte[] calcECDHSharedKey(byte[] clientPublic, byte[] serverPrivate) {
var p = SECNamedCurves.getByName("secp256r1");
var domainParams = new ECDomainParameters(p.getCurve(), p.getG(), p.getN(), p.getH(), p.getSeed());
var clientPoint = p.getCurve().decodePoint(clientPublic);
var clientParams = new ECPublicKeyParameters(clientPoint, domainParams);
var serverInteger = new BigInteger(serverPrivate);
var serverParams = new ECPrivateKeyParameters(serverInteger, domainParams);
var agreement = new ECDHBasicAgreement();
agreement.init(serverParams);
var result = agreement.calculateAgreement(clientParams);
return getUnsignedByteArray(result);
}
public static byte[] getUnsignedByteArray(BigInteger b) {
byte[] signedByteArray = b.toByteArray();
byte[] unsignedByteArray;
if (signedByteArray[0] == 0 && signedByteArray.length > 1) {
// Remove the leading zero byte
unsignedByteArray = new byte[signedByteArray.length - 1];
System.arraycopy(signedByteArray, 1, unsignedByteArray, 0, unsignedByteArray.length);
} else {
// No leading zero byte to remove, or it's a negative number
// which is not directly representable as an unsigned byte array without special handling
unsignedByteArray = signedByteArray;
}
return unsignedByteArray;
}
}

View File

@@ -0,0 +1,39 @@
package emu.nebula.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import emu.nebula.Nebula;
public class FileUtils {
public static void write(String dest, byte[] bytes) {
Path path = Paths.get(dest);
try {
Files.write(path, bytes);
} catch (IOException e) {
Nebula.getLogger().warn("Failed to write file: " + dest);
}
}
public static byte[] read(String dest) {
return read(Paths.get(dest));
}
public static byte[] read(Path path) {
try {
return Files.readAllBytes(path);
} catch (IOException e) {
Nebula.getLogger().warn("Failed to read file: " + path);
}
return new byte[0];
}
public static byte[] read(File file) {
return read(file.getPath());
}
}

View File

@@ -0,0 +1,94 @@
package emu.nebula.util;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.ItemDef;
public class Handbook {
public static void generate() {
// Temp vars
Map<String, String> languageKey = null;
List<Integer> list = null;
// Save to file
String file = "./Nebula Handbook.txt";
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8), true)) {
// Format date for header
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
var time = Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneId.systemDefault()).format(dtf);
// Header
writer.println("# Nebula " + GameConstants.VERSION + " Handbook");
writer.println("# Created " + time);
// Dump characters
writer.println(System.lineSeparator());
writer.println("# Characters");
list = GameData.getCharacterDataTable().keySet().intStream().sorted().boxed().toList();
languageKey = loadLanguageKey(CharacterDef.class);
for (int id : list) {
CharacterDef data = GameData.getCharacterDataTable().get(id);
writer.print(data.getId());
writer.print(" : ");
writer.println(languageKey.getOrDefault(data.getName(), data.getName()));
}
// Dump characters
writer.println(System.lineSeparator());
writer.println("# Items");
list = GameData.getItemDataTable().keySet().intStream().sorted().boxed().toList();
languageKey = loadLanguageKey(ItemDef.class);
for (int id : list) {
ItemDef data = GameData.getItemDataTable().get(id);
writer.print(data.getId());
writer.print(" : ");
writer.print(languageKey.getOrDefault(data.getTitle(), data.getTitle()));
writer.print(" [");
writer.print(data.getItemType());
writer.println("]");
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static Map<String, String> loadLanguageKey(Class<?> resourceClass) {
// Get type
ResourceType type = resourceClass.getAnnotation(ResourceType.class);
if (type == null) {
return Map.of();
}
// Load
Map<String, String> map = null;
try {
map = JsonUtils.loadToMap(Nebula.getConfig().getResourceDir() + "/language/en_US/" + type.name(), String.class, String.class);
} catch (Exception e) {
e.printStackTrace();
}
if (map == null) {
return Map.of();
}
return map;
}
}

View File

@@ -0,0 +1,112 @@
package emu.nebula.util;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import emu.nebula.Nebula;
public class JsonUtils {
private static final Gson gson = new GsonBuilder().setDateFormat("dd-MM-yyyy hh:mm:ss").setPrettyPrinting().create();
private static final Gson gsonCompact = new GsonBuilder().create();
public static Gson getGsonFactory() {
return gson;
}
/**
* Encode an object to a JSON string
*/
public static String encode(Object object) {
return gson.toJson(object);
}
/**
* Encode an object to a JSON string
* @param object
* @param compact
* @return
*/
public static String encode(Object object, boolean compact) {
return compact ? gsonCompact.toJson(object) : gson.toJson(object);
}
public static JsonElement encodeToElement(Object object) {
return gson.toJsonTree(object);
}
/**
* Safely JSON decodes a given string.
* @param jsonData The JSON-encoded data.
* @return JSON decoded data, or null if an exception occurred.
*/
public static <T> T decode(String jsonData, Class<T> classType) {
if (jsonData == null) {
return null;
}
try {
return gson.fromJson(jsonData, classType);
} catch (Exception ignored) {
return null;
}
}
public static <T> List<T> decodeList(String jsonData, Class<T> classType) {
if (jsonData == null) return null;
try {
return gson.fromJson(jsonData, TypeToken.getParameterized(List.class, classType).getType());
} catch (Exception ignored) {
return null;
}
}
public static <T> Set<T> decodeSet(String jsonData, Class<T> classType) {
if (jsonData == null) return null;
try {
return gson.fromJson(jsonData, TypeToken.getParameterized(Set.class, classType).getType());
} catch (Exception ignored) {
return null;
}
}
public static <T> T loadToClass(InputStreamReader fileReader, Class<T> classType) throws IOException {
return gson.fromJson(fileReader, classType);
}
public static <T> T loadToClass(File file, Class<T> classType) throws IOException {
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
return loadToClass(fileReader, classType);
}
}
public static <T> List<T> loadToList(InputStreamReader fileReader, Class<T> classType) throws IOException {
return gson.fromJson(fileReader, TypeToken.getParameterized(List.class, classType).getType());
}
public static <T> List<T> loadToList(String filename, Class<T> classType) throws IOException {
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
return loadToList(fileReader, classType);
}
}
public static <T1, T2> Map<T1, T2> loadToMap(InputStreamReader fileReader, Class<T1> keyType, Class<T2> valueType) throws IOException {
return gson.fromJson(fileReader, TypeToken.getParameterized(Map.class, keyType, valueType).getType());
}
public static <T1, T2> Map<T1, T2> loadToMap(String filename, Class<T1> keyType, Class<T2> valueType) throws IOException {
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
return loadToMap(fileReader, keyType, valueType);
} catch (FileNotFoundException ignored) {
Nebula.getLogger().error("File not found: {}.", filename);
return null;
}
}
}

View File

@@ -0,0 +1,24 @@
package emu.nebula.util;
public class Snowflake {
private static final long EPOCH = 1735689600000L; // Wednesday, January 1, 2025 12:00:00 AM (GMT)
private static int cachedTimestamp;
private static byte sequence;
public synchronized static int newUid() {
int timestamp = (int) ((System.currentTimeMillis() - EPOCH) / 1000);
if (cachedTimestamp != timestamp) {
sequence = 0;
cachedTimestamp = timestamp;
} else {
sequence++;
}
return (cachedTimestamp << 4) + sequence;
}
public synchronized static int toTimestamp(int snowflake) {
return (snowflake >> 4) + (int) (EPOCH / 1000);
}
}

View File

@@ -0,0 +1,265 @@
package emu.nebula.util;
import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import it.unimi.dsi.fastutil.ints.IntList;
public class Utils {
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
public static final Object EMPTY_OBJECT = new Object();
public static final int[] EMPTY_INT_ARRAY = new int[0];
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
public static String bytesToHex(byte[] bytes) {
return bytesToHex(bytes, 0);
}
public static String bytesToHex(byte[] bytes, int offset) {
if (bytes == null || (bytes.length - offset) <= 0) return "";
char[] hexChars = new char[(bytes.length - offset) * 2];
for (int j = offset; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
int h = j - offset;
hexChars[h * 2] = HEX_ARRAY[v >>> 4];
hexChars[h * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
public static byte[] hexToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
public static String capitalize(String s) {
StringBuilder sb = new StringBuilder(s);
sb.setCharAt(0, Character.toUpperCase(sb.charAt(0)));
return sb.toString();
}
public static String lowerCaseFirstChar(String s) {
StringBuilder sb = new StringBuilder(s);
sb.setCharAt(0, Character.toLowerCase(sb.charAt(0)));
return sb.toString();
}
/**
* Creates a string with the path to a file.
* @param path The path to the file.
* @return A path using the operating system's file separator.
*/
public static String toFilePath(String path) {
return path.replace("/", File.separator);
}
/**
* Checks if a file exists on the file system.
* @param path The path to the file.
* @return True if the file exists, false otherwise.
*/
public static boolean fileExists(String path) {
return new File(path).exists();
}
/**
* Creates a folder on the file system.
* @param path The path to the folder.
* @return True if the folder was created, false otherwise.
*/
public static boolean createFolder(String path) {
return new File(path).mkdirs();
}
public static long getCurrentSeconds() {
return Math.floorDiv(System.currentTimeMillis(), 1000);
}
public static int getMinPromotionForLevel(int level) {
return Math.max(Math.min((int) ((level - 11) / 10D), 6), 0);
}
/**
* Parses the string argument as a signed decimal integer. Returns a 0 if the string argument is not an integer.
*/
public static int parseSafeInt(String s) {
if (s == null) {
return 0;
}
int i = 0;
try {
i = Integer.parseInt(s);
} catch (Exception e) {
i = 0;
}
return i;
}
/**
* Parses the string argument as a signed decimal long. Returns a 0 if the string argument is not a long.
*/
public static long parseSafeLong(String s) {
if (s == null) {
return 0;
}
long i = 0;
try {
i = Long.parseLong(s);
} catch (Exception e) {
i = 0;
}
return i;
}
/**
* Add 2 integers without overflowing
*/
public static int safeAdd(int a, int b) {
return safeAdd(a, b, Integer.MAX_VALUE, 0);
}
public static int safeAdd(int a, int b, long max, long min) {
long sum = (long) a + (long) b;
if (sum > max) {
return (int) max;
} else if (sum < min) {
return (int) min;
}
return (int) sum;
}
/**
* Subtract 2 integers without overflowing
*/
public static int safeSubtract(int a, int b) {
return safeSubtract(a, b, Integer.MAX_VALUE, Integer.MIN_VALUE);
}
public static int safeSubtract(int a, int b, long max, long min) {
long sum = (long) a - (long) b;
if (sum > max) {
return (int) max;
} else if (sum < min) {
return (int) min;
}
return (int) sum;
}
public static double generateRandomDouble() {
return ThreadLocalRandom.current().nextDouble();
}
public static int randomRange(int min, int max) {
return ThreadLocalRandom.current().nextInt(min, max + 1);
}
public static int randomElement(int[] array) {
return array[ThreadLocalRandom.current().nextInt(0, array.length)];
}
public static <T> T randomElement(List<T> list) {
return list.get(ThreadLocalRandom.current().nextInt(0, list.size()));
}
public static int randomElement(IntList list) {
return list.getInt(ThreadLocalRandom.current().nextInt(0, list.size()));
}
/**
* Checks if an integer array contains a value
* @param array
* @param value The value to check for
*/
public static boolean arrayContains(int[] array, int value) {
for (int i = 0; i < array.length; i++) {
if (array[i] == value) return true;
}
return false;
}
public static boolean arrayContains(Integer[] array, int value) {
for (Integer element : array) {
if (element != null && element.equals(value)) {
return true;
}
}
return false;
}
public static boolean arrayContains(List<Integer> list, int value) {
for (Integer element : list) {
if (element.equals(value)) {
return true;
}
}
return false;
}
public static int[] convertListToIntArray(List<Integer> list) {
// Create an int array with the same size as the list
int[] intArray = new int[list.size()];
// Iterate over the list and populate the int array
for (int i = 0; i < list.size(); i++) {
intArray[i] = list.get(i);
}
return intArray;
}
/**
* Base64 encodes a given byte array.
* @param toEncode An array of bytes.
* @return A base64 encoded string.
*/
public static String base64Encode(byte[] toEncode) {
return Base64.getEncoder().encodeToString(toEncode);
}
/**
* Base64 decodes a given string.
* @param toDecode A base64 encoded string.
* @return An array of bytes.
*/
public static byte[] base64Decode(String toDecode) {
return Base64.getDecoder().decode(toDecode);
}
/**
* Checks if a port is open on a given host.
*
* @param host The host to check.
* @param port The port to check.
* @return True if the port is open, false otherwise.
*/
public static boolean isPortOpen(String host, int port) {
try (var serverSocket = new ServerSocket()) {
serverSocket.setReuseAddress(false);
serverSocket.bind(new InetSocketAddress(InetAddress.getByName(host), port), 1);
return true;
} catch (Exception ex) {
return false;
}
}
}