From 2a5b8270818dca050feab97c4b066f22c16c8344 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Tue, 30 Dec 2025 23:13:11 -0600 Subject: [PATCH 1/6] Fixed overwriting existing config Fixed overwriting existing config file on every start of the mod. Now changes to the config will actually persist. --- src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java | 2 +- src/main/java/cn/alini/offlineauth/Offlineauth.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index c747775..4d962a0 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -44,7 +44,7 @@ public class OfflineAuthHandler { private static final Gson gson = new Gson(); private static final Map autoLoginMap = new HashMap<>(); private static final Map failMap = new HashMap<>(); - static { loadAutoLogin(); loadFail(); } + static { config.load(); loadAutoLogin(); loadFail(); } private static boolean isOfflinePlayer(ServerPlayer player) { return !TrueuuidApi.isPremium(player.getName().getString().toLowerCase(Locale.ROOT)); diff --git a/src/main/java/cn/alini/offlineauth/Offlineauth.java b/src/main/java/cn/alini/offlineauth/Offlineauth.java index 38d47b5..9f8dc83 100644 --- a/src/main/java/cn/alini/offlineauth/Offlineauth.java +++ b/src/main/java/cn/alini/offlineauth/Offlineauth.java @@ -13,6 +13,5 @@ public class Offlineauth { public Offlineauth() { //mod成功加载日志 LOGGER.info("OfflineAuth mod loaded successfully!"); - new AuthConfig().save(); } } From 0ec9a6098a09b2542c96e13f31b12eb7006a31ed Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Tue, 30 Dec 2025 23:15:41 -0600 Subject: [PATCH 2/6] Update trueuuid version and mod version trueuuid library from 1.0.2 to 1.0.5 in build.gradle and changed mod_version to 1.0.3 in gradle.properties. --- build.gradle | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7997ff1..0185c69 100644 --- a/build.gradle +++ b/build.gradle @@ -149,7 +149,7 @@ dependencies { // If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"], // then special handling is done to allow a setup of a vanilla dependency without the use of an external repository. minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" - implementation files('libs/trueuuid-1.0.2.jar') + implementation files('libs/trueuuid-1.0.5.jar') // Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}") diff --git a/gradle.properties b/gradle.properties index 261059b..41c7b08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,7 +38,7 @@ mod_name=OfflineAuth # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=GNU LGPL 3.0 # The mod version. See https://semver.org/ -mod_version=1.0.1 +mod_version=1.0.3 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html From 1efb2784bb21a94d197cad55c95d1c2b18da904a Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 18:53:19 -0600 Subject: [PATCH 3/6] Password hashing Passwords are now more securely stored using PBKDF2WithHmacSHA256 with a random salt. Added migration system to convert existing plaintext passwords from the old auth.json file to hashed format in auth_hash.json --- .../cn/alini/offlineauth/JsonAuthStorage.java | 88 +++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java b/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java index 8712dfb..a1dee08 100644 --- a/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java +++ b/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java @@ -3,19 +3,33 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; import java.io.*; import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import java.util.Base64; +import java.util.Arrays; public class JsonAuthStorage { private static final String DIR = "config/offlineauth"; - private static final String FILE_NAME = "auth.json"; + private static final String FILE_NAME = "auth_hash.json"; + private static final String OLD_FILE_NAME = "auth.json"; private static final Path FILE_PATH = Path.of(DIR, FILE_NAME); private static final Gson gson = new Gson(); + // PBKDF2 constants for password hashing + private static final String ALGORITHM = "PBKDF2WithHmacSHA256"; + private static final int ITERATIONS = 10000; + private static final int KEY_LENGTH = 256; + private static final String SALT_SEPARATOR = ":"; + private Map credentials = new HashMap<>(); private long lastModified = -1; @@ -41,9 +55,33 @@ private void load() { credentials = new HashMap<>(); File dir = new File(DIR); if (!dir.exists()) dir.mkdirs(); + File file = FILE_PATH.toFile(); + File oldFile = Path.of(DIR, OLD_FILE_NAME).toFile(); + + // Old to New Migration logic: If new file doesn't exist but old one does + if (!file.exists() && oldFile.exists()) { + try (Reader reader = new FileReader(oldFile)) { + Type type = new TypeToken>(){}.getType(); + Map map = gson.fromJson(reader, type); + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + // Hash existing plaintext passwords + credentials.put(entry.getKey(), hashPassword(entry.getValue())); + } + } + save(); // Save to new auth_hash.json + // Rename old file to prevent confusion + oldFile.renameTo(new File(dir, OLD_FILE_NAME + ".migrated")); + } catch (Exception e) { + e.printStackTrace(); + } + lastModified = file.exists() ? file.lastModified() : -1; + return; + } + if (!file.exists()) { - save(); // 写一个空文件 + save(); // 写一个空文件 (Write an empty file) lastModified = file.exists() ? file.lastModified() : -1; return; } @@ -76,20 +114,60 @@ public boolean isRegistered(String name) { public void register(String name, String password) { reloadIfChanged(); - credentials.put(name, password); + credentials.put(name, hashPassword(password)); save(); } public boolean checkPassword(String name, String password) { reloadIfChanged(); - return credentials.containsKey(name) && credentials.get(name).equals(password); + return credentials.containsKey(name) && verifyPassword(password, credentials.get(name)); } public void changePassword(String name, String newPassword) { reloadIfChanged(); if (credentials.containsKey(name)) { - credentials.put(name, newPassword); + credentials.put(name, hashPassword(newPassword)); save(); } } + + private String hashPassword(String password) { + try { + // Generate random 16 byte salt + byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + + // Hash using PBKDF2 with the generated salt + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH); + SecretKeyFactory skf = SecretKeyFactory.getInstance(ALGORITHM); + byte[] hash = skf.generateSecret(spec).getEncoded(); + + // Format: salt:hash (both Base64) + return Base64.getEncoder().encodeToString(salt) + SALT_SEPARATOR + Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + return null; + } + } + + private boolean verifyPassword(String inputPassword, String storedHash) { + try { + // Split value into salt and hash + String[] parts = storedHash.split(SALT_SEPARATOR); + if (parts.length != 2) return false; + byte[] salt = Base64.getDecoder().decode(parts[0]); + byte[] hash = Base64.getDecoder().decode(parts[1]); + + // Hash the input password using the same salt + PBEKeySpec spec = new PBEKeySpec(inputPassword.toCharArray(), salt, ITERATIONS, KEY_LENGTH); + SecretKeyFactory skf = SecretKeyFactory.getInstance(ALGORITHM); + byte[] inputHash = skf.generateSecret(spec).getEncoded(); + + // Compare new hash with stored hash + return Arrays.equals(hash, inputHash); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } } \ No newline at end of file From 3a1c5af978eb76e68005bb4fd4df7c2d4d18f87b Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 19:10:29 -0600 Subject: [PATCH 4/6] Fix rename fail and load logic --- .../java/cn/alini/offlineauth/JsonAuthStorage.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java b/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java index a1dee08..28b0050 100644 --- a/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java +++ b/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java @@ -41,8 +41,8 @@ public JsonAuthStorage() { private void reloadIfChanged() { File file = FILE_PATH.toFile(); if (!file.exists()) { - credentials = new HashMap<>(); - lastModified = -1; + // File missing, might need migration or it's just empty. Let load() handle it. + load(); return; } long lm = file.lastModified(); @@ -61,6 +61,7 @@ private void load() { // Old to New Migration logic: If new file doesn't exist but old one does if (!file.exists() && oldFile.exists()) { + boolean migrationSuccess = false; try (Reader reader = new FileReader(oldFile)) { Type type = new TypeToken>(){}.getType(); Map map = gson.fromJson(reader, type); @@ -70,12 +71,15 @@ private void load() { credentials.put(entry.getKey(), hashPassword(entry.getValue())); } } - save(); // Save to new auth_hash.json - // Rename old file to prevent confusion - oldFile.renameTo(new File(dir, OLD_FILE_NAME + ".migrated")); + migrationSuccess = true; } catch (Exception e) { e.printStackTrace(); } + + if (migrationSuccess) { + save(); // Save to new auth_hash.json + oldFile.renameTo(new File(dir, OLD_FILE_NAME + ".migrated")); // Rename old file + } lastModified = file.exists() ? file.lastModified() : -1; return; } From 3914c49c1f4e3d9f78fdc70a890916927d8df6bd Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 19:26:53 -0600 Subject: [PATCH 5/6] Replace printStackTrace with logging Replaced all e.printStackTrace() with proper logger error calls --- .../java/cn/alini/offlineauth/JsonAuthStorage.java | 14 ++++++++------ .../cn/alini/offlineauth/OfflineAuthHandler.java | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java b/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java index 28b0050..30f89fb 100644 --- a/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java +++ b/src/main/java/cn/alini/offlineauth/JsonAuthStorage.java @@ -2,6 +2,8 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; @@ -23,6 +25,7 @@ public class JsonAuthStorage { private static final String OLD_FILE_NAME = "auth.json"; private static final Path FILE_PATH = Path.of(DIR, FILE_NAME); private static final Gson gson = new Gson(); + private static final Logger LOGGER = LogUtils.getLogger(); // PBKDF2 constants for password hashing private static final String ALGORITHM = "PBKDF2WithHmacSHA256"; @@ -73,7 +76,7 @@ private void load() { } migrationSuccess = true; } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Failed to migrate old auth.json", e); } if (migrationSuccess) { @@ -94,7 +97,7 @@ private void load() { Map map = gson.fromJson(reader, type); if (map != null) credentials.putAll(map); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Failed to load auth_hash.json", e); } lastModified = file.lastModified(); } @@ -105,7 +108,7 @@ private void save() { try (Writer writer = new FileWriter(FILE_PATH.toFile())) { gson.toJson(credentials, writer); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to save auth_hash.json", e); } File file = FILE_PATH.toFile(); lastModified = file.exists() ? file.lastModified() : -1; @@ -149,8 +152,7 @@ private String hashPassword(String password) { // Format: salt:hash (both Base64) return Base64.getEncoder().encodeToString(salt) + SALT_SEPARATOR + Base64.getEncoder().encodeToString(hash); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - e.printStackTrace(); - return null; + throw new RuntimeException("Failed to hash password", e); } } @@ -170,7 +172,7 @@ private boolean verifyPassword(String inputPassword, String storedHash) { // Compare new hash with stored hash return Arrays.equals(hash, inputHash); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Failed to verify password", e); return false; } } diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index 4d962a0..88cf7f0 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -76,13 +76,13 @@ private static void loadAutoLogin() { Type t = new TypeToken>(){}.getType(); Map m = gson.fromJson(r, t); if (m != null) autoLoginMap.putAll(m); - } catch (Exception e) { e.printStackTrace(); } + } catch (Exception e) { LOGGER.error("Failed to load autologin.json", e); } } private static void saveAutoLogin() { File f = new File(AUTOLOGIN_FILE); try (Writer w = new FileWriter(f)) { gson.toJson(autoLoginMap, w); - } catch (Exception e) { e.printStackTrace(); } + } catch (Exception e) { LOGGER.error("Failed to save autologin.json", e); } } private static void loadFail() { File f = new File(FAIL_FILE); @@ -91,13 +91,13 @@ private static void loadFail() { Type t = new TypeToken>(){}.getType(); Map m = gson.fromJson(r, t); if (m != null) failMap.putAll(m); - } catch (Exception e) { e.printStackTrace(); } + } catch (Exception e) { LOGGER.error("Failed to load fail.json", e); } } private static void saveFail() { File f = new File(FAIL_FILE); try (Writer w = new FileWriter(f)) { gson.toJson(failMap, w); - } catch (Exception e) { e.printStackTrace(); } + } catch (Exception e) { LOGGER.error("Failed to save fail.json", e); } } @SubscribeEvent From 31c73a48e605db5e3510db26eecc92c29d22e9aa Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Fri, 2 Jan 2026 08:38:47 -0600 Subject: [PATCH 6/6] Update README - Password Hashing --- README.md | 2 +- README_zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 184112e..b991b9d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Edit `config/offlineauth/config.json` to customize: ## Security Notice - **Auto-login (same IP)** is convenient, but insecure on public computers/networks. Always warn players to protect their accounts. -- Passwords are stored in plain text (by default), use at your own risk or extend for encryption. +- Passwords are securely hashed using PBKDF2 with a random salt and stored under `config/offlineauth/auth_hash.json` on the server. ## How to Install diff --git a/README_zh-CN.md b/README_zh-CN.md index df58081..a73281e 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -37,7 +37,7 @@ ## 安全提醒 - **自动登录(同IP)功能** 虽然便捷,但在网吧/公共电脑环境下存在被盗号风险。务必提醒玩家注意账号安全。 -- 密码默认明文存储,仅供学习/小型服使用,如需高安全请自行扩展加密方案。 +- 密码现已使用 PBKDF2 算法和随机盐进行安全哈希,并存储于服务器的 `config/offlineauth/auth_hash.json` 中。 ## 安装方法