diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java index efe637530..51f0684ba 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -6,6 +6,7 @@ import emu.grasscutter.game.Account; import emu.grasscutter.server.http.objects.ComboTokenResJson; import emu.grasscutter.server.http.objects.LoginResultJson; +import static emu.grasscutter.Configuration.ACCOUNT; import static emu.grasscutter.utils.Language.translate; /** @@ -13,12 +14,20 @@ import static emu.grasscutter.utils.Language.translate; * Allows all users to access any account. */ public final class DefaultAuthentication implements AuthenticationSystem { - private final Authenticator passwordAuthenticator = new PasswordAuthenticator(); - private final Authenticator tokenAuthenticator = new TokenAuthenticator(); - private final Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); - private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication(); - private final OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication(); - + private Authenticator passwordAuthenticator; + private Authenticator tokenAuthenticator = new TokenAuthenticator(); + private Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); + private ExternalAuthenticator externalAuthenticator = new ExternalAuthentication(); + private OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication(); + + public DefaultAuthentication() { + if(ACCOUNT.EXPERIMENTAL_RealPassword) { + passwordAuthenticator = new ExperimentalPasswordAuthenticator(); + } else { + passwordAuthenticator = new PasswordAuthenticator(); + } + } + @Override public void createAccount(String username, String password) { // Unhandled. The default authenticator doesn't store passwords. diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index 905535380..6c5585ec8 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -1,10 +1,19 @@ package emu.grasscutter.auth; +import at.favre.lib.crypto.bcrypt.BCrypt; import emu.grasscutter.Grasscutter; import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.server.http.objects.*; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; + +import javax.crypto.Cipher; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; import static emu.grasscutter.Configuration.*; import static emu.grasscutter.utils.Language.translate; @@ -13,19 +22,20 @@ import static emu.grasscutter.utils.Language.translate; * A class containing default authenticators. */ public final class DefaultAuthenticators { - + /** * Handles the authentication request from the username and password form. */ public static class PasswordAuthenticator implements Authenticator { - @Override public LoginResultJson authenticate(AuthenticationRequest request) { + @Override + public LoginResultJson authenticate(AuthenticationRequest request) { var response = new LoginResultJson(); - + var requestData = request.getPasswordRequest(); assert requestData != null; // This should never be null. int playerCount = Grasscutter.getGameServer().getPlayers().size(); - boolean successfulLogin = false; + boolean successfulLogin = false; String address = request.getRequest().ip(); String responseMessage = translate("messages.dispatch.account.username_error"); String loggerMessage = ""; @@ -34,12 +44,12 @@ public final class DefaultAuthenticators { Account account = DatabaseHelper.getAccountByName(requestData.account); if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) { // Check if account exists. - if(account == null && ACCOUNT.autoCreate) { + if (account == null && ACCOUNT.autoCreate) { // This account has been created AUTOMATICALLY. There will be no permissions added. account = DatabaseHelper.createAccountWithUid(requestData.account, 0); // Check if the account was created successfully. - if(account == null) { + if (account == null) { responseMessage = translate("messages.dispatch.account.username_create_error"); Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", address)); } else { @@ -49,9 +59,9 @@ public final class DefaultAuthenticators { // Log the creation. Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid)); } - } else if(account != null) + } else if (account != null) successfulLogin = true; - else + else loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address); } else { @@ -59,9 +69,120 @@ public final class DefaultAuthenticators { loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address); } - + // Set response data. - if(successfulLogin) { + if (successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.generateSessionKey(); + response.data.account.email = account.getEmail(); + + loggerMessage = translate("messages.dispatch.account.login_success", address, account.getId()); + } else { + response.retcode = -201; + response.message = responseMessage; + + } + Grasscutter.getLogger().info(loggerMessage); + + return response; + } + } + + public static class ExperimentalPasswordAuthenticator implements Authenticator { + @Override + public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + var requestData = request.getPasswordRequest(); + assert requestData != null; // This should never be null. + int playerCount = Grasscutter.getGameServer().getPlayers().size(); + + boolean successfulLogin = false; + String address = request.getRequest().ip(); + String responseMessage = translate("messages.dispatch.account.username_error"); + String loggerMessage = ""; + String decryptedPassword = ""; + + // Get Password + if (GAME_OPTIONS.uaPatchCompatible) { + // Make sure your patch can send passwords in plain text + decryptedPassword = request.getPasswordRequest().password; + } else { + try { + byte[] key = FileUtils.readResource("/keys/auth_private-key.der"); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPrivateKey private_key = (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + + cipher.init(Cipher.DECRYPT_MODE, private_key); + + decryptedPassword = new String(cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)), StandardCharsets.UTF_8); + } catch (Exception ignored) { + ignored.printStackTrace(); + } + } + + if (decryptedPassword == null) { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_error", address); + responseMessage = translate("messages.dispatch.account.password_error"); + } + + // Get account from database. + Account account = DatabaseHelper.getAccountByName(requestData.account); + if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) { + // Check if account exists. + if (account == null && ACCOUNT.autoCreate) { + // This account has been created AUTOMATICALLY. There will be no permissions added. + if (decryptedPassword.length() >= 8) { + account = DatabaseHelper.createAccountWithUid(requestData.account, 0); + account.setPassword(BCrypt.withDefaults().hashToString(12, decryptedPassword.toCharArray())); + account.save(); + + // Check if the account was created successfully. + if (account == null) { + responseMessage = translate("messages.dispatch.account.username_create_error"); + loggerMessage = translate("messages.dispatch.account.account_login_create_error", address); + } else { + // Continue with login. + successfulLogin = true; + + // Log the creation. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid)); + } + } else { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_error", address); + responseMessage = translate("messages.dispatch.account.password_length_error"); + } + } else if (account != null) { + if (account.getPassword() != null && !account.getPassword().isEmpty()) { + if (BCrypt.verifyer().verify(decryptedPassword.toCharArray(), account.getPassword()).verified) { + successfulLogin = true; + } else { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_error", address); + responseMessage = translate("messages.dispatch.account.password_error"); + } + } else { + successfulLogin = false; + loggerMessage = translate("messages.dispatch.account.login_password_storage_error", address); + responseMessage = translate("password_storage_error"); + } + } else { + loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address); + } + } else { + responseMessage = translate("messages.dispatch.account.server_max_player_limit"); + loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address); + } + + + // Set response data. + if (successfulLogin) { response.message = "OK"; response.data.account.uid = account.getId(); response.data.account.token = account.generateSessionKey(); @@ -83,9 +204,10 @@ public final class DefaultAuthenticators { * Handles the authentication request from the game when using a registry token. */ public static class TokenAuthenticator implements Authenticator { - @Override public LoginResultJson authenticate(AuthenticationRequest request) { + @Override + public LoginResultJson authenticate(AuthenticationRequest request) { var response = new LoginResultJson(); - + var requestData = request.getTokenRequest(); assert requestData != null; @@ -106,7 +228,7 @@ public final class DefaultAuthenticators { successfulLogin = account != null && account.getSessionKey().equals(requestData.token); // Set response data. - if(successfulLogin) { + if (successfulLogin) { response.message = "OK"; response.data.account.uid = account.getId(); response.data.account.token = account.getSessionKey(); @@ -138,13 +260,15 @@ public final class DefaultAuthenticators { * Handles the authentication request from the game when using a combo token/session key. */ public static class SessionKeyAuthenticator implements Authenticator { - @Override public ComboTokenResJson authenticate(AuthenticationRequest request) { - var response = new ComboTokenResJson(); - + @Override + public ComboTokenResJson authenticate(AuthenticationRequest request) { + var response = new ComboTokenResJson(); + var requestData = request.getSessionKeyRequest(); var loginData = request.getSessionKeyData(); - assert requestData != null; assert loginData != null; - + assert requestData != null; + assert loginData != null; + boolean successfulLogin; String address = request.getRequest().ip(); String loggerMessage; @@ -158,7 +282,7 @@ public final class DefaultAuthenticators { successfulLogin = account != null && account.getSessionKey().equals(loginData.token); // Set response data. - if(successfulLogin) { + if (successfulLogin) { response.message = "OK"; response.data.open_id = account.getId(); response.data.combo_id = "157795300"; @@ -190,17 +314,20 @@ public final class DefaultAuthenticators { * Handles authentication requests from external sources. */ public static class ExternalAuthentication implements ExternalAuthenticator { - @Override public void handleLogin(AuthenticationRequest request) { + @Override + public void handleLogin(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handleAccountCreation(AuthenticationRequest request) { + @Override + public void handleAccountCreation(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handlePasswordReset(AuthenticationRequest request) { + @Override + public void handlePasswordReset(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } @@ -210,17 +337,20 @@ public final class DefaultAuthenticators { * Handles authentication requests from OAuth sources. */ public static class OAuthAuthentication implements OAuthAuthenticator { - @Override public void handleLogin(AuthenticationRequest request) { + @Override + public void handleLogin(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handleRedirection(AuthenticationRequest request, ClientType type) { + @Override + public void handleRedirection(AuthenticationRequest request, ClientType type) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } - @Override public void handleTokenProcess(AuthenticationRequest request) { + @Override + public void handleTokenProcess(AuthenticationRequest request) { assert request.getResponse() != null; request.getResponse().send("Authentication is not available with the default authentication method."); } diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index 0d50b3787..4f96b986f 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -1,5 +1,7 @@ package emu.grasscutter.command.commands; +import at.favre.lib.crypto.bcrypt.BCrypt; +import emu.grasscutter.Configuration; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; @@ -11,7 +13,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "account", usage = "account [uid]", description = "commands.account.description", targetRequirement = Command.TargetRequirement.NONE) +@Command(label = "account", usage = "account [uid|password] [uid] ", description = "commands.account.description", targetRequirement = Command.TargetRequirement.NONE) public final class AccountCommand implements CommandHandler { @Override @@ -35,13 +37,38 @@ public final class AccountCommand implements CommandHandler { return; case "create": int uid = 0; - if (args.size() > 2) { - try { - uid = Integer.parseInt(args.get(2)); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, translate(sender, "commands.account.invalid")); + String password = ""; + + if(Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) { + if(args.size() < 3) { + CommandHandler.sendMessage(null, "EXPERIMENTAL_RealPassword requires a password argument"); + CommandHandler.sendMessage(null, "Usage: account create [uid]"); + return; } + password = args.get(2); + + if (args.size() == 4) { + try { + uid = Integer.parseInt(args.get(3)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(null, translate(sender, "commands.account.invalid")); + if(Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) { + CommandHandler.sendMessage(null, "EXPERIMENTAL_RealPassword requires argument 2 to be a password, not a uid"); + CommandHandler.sendMessage(null, "Usage: account create [uid]"); + } + return; + } + } + } else { + if (args.size() > 2) { + try { + uid = Integer.parseInt(args.get(2)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(null, translate(sender, "commands.account.invalid")); + return; + } + } } emu.grasscutter.game.Account account = DatabaseHelper.createAccountWithUid(username, uid); @@ -49,6 +76,9 @@ public final class AccountCommand implements CommandHandler { CommandHandler.sendMessage(null, translate(sender, "commands.account.exists")); return; } else { + if(Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) { + account.setPassword(BCrypt.withDefaults().hashToString(12, password.toCharArray())); + } account.addPermission("*"); account.save(); // Save account to database. @@ -74,6 +104,37 @@ public final class AccountCommand implements CommandHandler { // Finally, we do the actual deletion. DatabaseHelper.deleteAccount(toDelete); CommandHandler.sendMessage(null, translate(sender, "commands.account.delete")); + return; + case "resetpass": + if(Configuration.ACCOUNT.EXPERIMENTAL_RealPassword != true) { + CommandHandler.sendMessage(null, "resetpass requires EXPERIMENTAL_RealPassword to be true."); + return; + } + + if(args.size() != 3) { + CommandHandler.sendMessage(null, "Invalid Args"); + CommandHandler.sendMessage(null, "Usage: account resetpass "); + return; + } + + Account toUpdate = DatabaseHelper.getAccountByName(username); + + if (toUpdate == null) { + CommandHandler.sendMessage(null, translate(sender, "commands.account.no_account")); + return; + } + + // Get the player for the account. + // If that player is currently online, we kick them before proceeding with the deletion. + Player uPlayer = Grasscutter.getGameServer().getPlayerByAccountId(toUpdate.getId()); + if (uPlayer != null) { + uPlayer.getSession().close(); + } + + toUpdate.setPassword(BCrypt.withDefaults().hashToString(12, args.get(2).toCharArray())); + toUpdate.save(); + CommandHandler.sendMessage(null, "Password Updated."); + return; } } } diff --git a/src/main/java/emu/grasscutter/server/http/HttpServer.java b/src/main/java/emu/grasscutter/server/http/HttpServer.java index bcc020ed7..34d2059a1 100644 --- a/src/main/java/emu/grasscutter/server/http/HttpServer.java +++ b/src/main/java/emu/grasscutter/server/http/HttpServer.java @@ -11,7 +11,7 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; import java.io.File; -import java.io.IOException; +import java.io.UnsupportedEncodingException; import static emu.grasscutter.Configuration.*; import static emu.grasscutter.utils.Language.translate; @@ -126,15 +126,16 @@ public final class HttpServer { /** * Starts listening on the HTTP server. + * @throws UnsupportedEncodingException */ - public void start() { + public void start() throws UnsupportedEncodingException { // Attempt to start the HTTP server. if(HTTP_INFO.bindAddress.equals("")){ this.express.listen(HTTP_INFO.bindPort); }else{ this.express.listen(HTTP_INFO.bindAddress, HTTP_INFO.bindPort); } - + // Log bind information. Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(this.express.raw().port()))); } diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java index f2d1cc4ca..c1fdfc7a0 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -1,33 +1,31 @@ package emu.grasscutter.server.http.dispatch; import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerRunMode; -import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*; -import emu.grasscutter.net.proto.RegionInfoOuterClass; +import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; import emu.grasscutter.server.http.Router; +import emu.grasscutter.server.http.objects.QueryCurRegionRspJson; import emu.grasscutter.utils.Crypto; -import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.Utils; import express.Express; import express.http.Request; import express.http.Response; import io.javalin.Javalin; -import java.io.File; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.Map; +import javax.crypto.Cipher; +import java.io.ByteArrayOutputStream; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.security.Signature; + import static emu.grasscutter.Configuration.*; -import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*; +import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp; /** * Handles requests related to region queries. @@ -35,7 +33,7 @@ import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*; public final class RegionHandler implements Router { private static final Map regions = new ConcurrentHashMap<>(); private static String regionListResponse; - + public RegionHandler() { try { // Read & initialize region data. this.initialize(); @@ -51,33 +49,33 @@ public final class RegionHandler implements Router { String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort); - + // Create regions. List servers = new ArrayList<>(); List usedNames = new ArrayList<>(); // List to check for potential naming conflicts. - + var configuredRegions = new ArrayList<>(List.of(DISPATCH_INFO.regions)); if(SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) { Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state."); System.exit(1); - } else if (configuredRegions.size() == 0) + } else if (configuredRegions.size() == 0) configuredRegions.add(new Region("os_usa", DISPATCH_INFO.defaultName, - lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress), - lr(GAME_INFO.accessPort, GAME_INFO.bindPort))); - + lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress), + lr(GAME_INFO.accessPort, GAME_INFO.bindPort))); + configuredRegions.forEach(region -> { if (usedNames.contains(region.Name)) { Grasscutter.getLogger().error("Region name already in use."); return; } - + // Create a region identifier. var identifier = RegionSimpleInfo.newBuilder() .setName(region.Name).setTitle(region.Title).setType("DEV_PUBLIC") .setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name) .build(); usedNames.add(region.Name); servers.add(identifier); - + // Create a region info object. var regionInfo = RegionInfo.newBuilder() .setGateserverIp(region.Ip).setGateserverPort(region.Port) @@ -87,22 +85,22 @@ public final class RegionHandler implements Router { var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build(); regions.put(region.Name, new RegionData(updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray()))); }); - + // Create a config object. byte[] customConfig = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes(); Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key. - + // Create an updated region list. QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder() .addAllRegionList(servers) .setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)) .setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig)) .setEnableLoginPc(true).build(); - + // Set the region list response. regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray()); } - + @Override public void applyRoutes(Express express, Javalin handle) { express.get("/query_region_list", RegionHandler::queryRegionList); express.get("/query_cur_region/:region", RegionHandler::queryCurrentRegion ); @@ -116,7 +114,7 @@ public final class RegionHandler implements Router { QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); event.call(); // Respond with event result. response.send(event.getRegionList()); - + // Log to console. Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", request.ip())); } @@ -127,19 +125,72 @@ public final class RegionHandler implements Router { private static void queryCurrentRegion(Request request, Response response) { // Get region to query. String regionName = request.params("region"); - + String versionName = request.query("version"); + var region = regions.get(regionName); + // Get region data. String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; if (request.query().values().size() > 0) { - var region = regions.get(regionName); - if(region != null) regionData = region.getBase64(); + if(region != null) + regionData = region.getBase64(); } - - // Invoke event. - QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); - // Respond with event result. - response.send(event.getRegionInfo()); + if( versionName.contains("2.7.5") || versionName.contains("2.8.")) { + try { + QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); + + if (GAME_OPTIONS.uaPatchCompatible) { + // More love for UA Patch players + + var rsp = new QueryCurRegionRspJson(); + + rsp.content = event.getRegionInfo(); + rsp.sign = "TW9yZSBsb3ZlIGZvciBVQSBQYXRjaCBwbGF5ZXJz"; + + response.send(rsp); + return; + } + + String key_id = request.query("key_id"); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, key_id.equals("3") ? Crypto.CUR_OS_ENCRYPT_KEY : Crypto.CUR_CN_ENCRYPT_KEY); + var regionInfo = Utils.base64Decode(event.getRegionInfo()); + + //Encrypt regionInfo in chunks + ByteArrayOutputStream encryptedRegionInfoStream = new ByteArrayOutputStream(); + + //Thank you so much GH Copilot + int chunkSize = 256 - 11; + int regionInfoLength = regionInfo.length; + int numChunks = (int) Math.ceil(regionInfoLength / (double) chunkSize); + + for (int i = 0; i < numChunks; i++) { + byte[] chunk = Arrays.copyOfRange(regionInfo, i * chunkSize, Math.min((i + 1) * chunkSize, regionInfoLength)); + byte[] encryptedChunk = cipher.doFinal(chunk); + encryptedRegionInfoStream.write(encryptedChunk); + } + + Signature privateSignature = Signature.getInstance("SHA256withRSA"); + privateSignature.initSign(Crypto.CUR_SIGNING_KEY); + privateSignature.update(regionInfo); + + var rsp = new QueryCurRegionRspJson(); + + rsp.content = Utils.base64Encode(encryptedRegionInfoStream.toByteArray()); + rsp.sign = Utils.base64Encode(privateSignature.sign()); + + response.send(rsp); + } + catch (Exception e) { + Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e); + } + } + else { + // Invoke event. + QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); + // Respond with event result. + response.send(event.getRegionInfo()); + } // Log to console. Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region/%s", request.ip(), regionName)); } @@ -172,4 +223,4 @@ public final class RegionHandler implements Router { public static QueryCurrRegionHttpRsp getCurrentRegion() { return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null; } -} +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/http/objects/QueryCurRegionRspJson.java b/src/main/java/emu/grasscutter/server/http/objects/QueryCurRegionRspJson.java new file mode 100644 index 000000000..39fd24da7 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/QueryCurRegionRspJson.java @@ -0,0 +1,6 @@ +package emu.grasscutter.server.http.objects; + +public class QueryCurRegionRspJson { + public String content; + public String sign; +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java index 68ac9af9c..b598f6d1e 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java @@ -1,6 +1,7 @@ package emu.grasscutter.server.packet.recv; import static emu.grasscutter.Configuration.ACCOUNT; +import static emu.grasscutter.Configuration.GAME_OPTIONS; import emu.grasscutter.Grasscutter; import emu.grasscutter.database.DatabaseHelper; @@ -14,6 +15,14 @@ import emu.grasscutter.server.event.game.PlayerCreationEvent; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession.SessionState; import emu.grasscutter.server.packet.send.PacketGetPlayerTokenRsp; +import emu.grasscutter.utils.ByteHelper; +import emu.grasscutter.utils.Crypto; +import emu.grasscutter.utils.Utils; + +import javax.crypto.Cipher; + +import java.nio.ByteBuffer; +import java.security.Signature; @Opcodes(PacketOpcodes.GetPlayerTokenReq) public class HandlerGetPlayerTokenReq extends PacketHandler { @@ -90,8 +99,45 @@ public class HandlerGetPlayerTokenReq extends PacketHandler { session.setUseSecretKey(true); session.setState(SessionState.WAITING_FOR_LOGIN); - // Send packet - session.send(new PacketGetPlayerTokenRsp(session)); - } + // Only >= 2.7.50 has this + if (req.getKeyId() > 0) { + if (GAME_OPTIONS.uaPatchCompatible) { + // More love for ua patch plz 😭 + byte[] clientBytes = Utils.base64Decode(req.getClientSeed()); + byte[] seed = ByteHelper.longToBytes(Crypto.ENCRYPT_SEED); + Crypto.xor(clientBytes, seed); + + String base64str = Utils.base64Encode(clientBytes); + + session.send(new PacketGetPlayerTokenRsp(session, base64str, "bm90aGluZyBoZXJl")); + return; + } + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, Crypto.CUR_SIGNING_KEY); + + var client_seed_encrypted = Utils.base64Decode(req.getClientSeed()); + var client_seed = ByteBuffer.wrap(cipher.doFinal(client_seed_encrypted)) + .getLong(); + + byte[] seed_bytes = ByteBuffer.wrap(new byte[8]) + .putLong(Crypto.ENCRYPT_SEED ^ client_seed) + .array(); + + //Kind of a hack, but whatever + cipher.init(Cipher.ENCRYPT_MODE, req.getKeyId() == 3 ? Crypto.CUR_OS_ENCRYPT_KEY : Crypto.CUR_CN_ENCRYPT_KEY); + var seed_encrypted = cipher.doFinal(seed_bytes); + + Signature privateSignature = Signature.getInstance("SHA256withRSA"); + privateSignature.initSign(Crypto.CUR_SIGNING_KEY); + privateSignature.update(seed_bytes); + + session.send(new PacketGetPlayerTokenRsp(session, Utils.base64Encode(seed_encrypted), Utils.base64Encode(privateSignature.sign()))); + } + else { + // Send packet + session.send(new PacketGetPlayerTokenRsp(session)); + } + } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java index 1377920e5..8fb558acb 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java @@ -10,45 +10,70 @@ import emu.grasscutter.utils.Crypto; public class PacketGetPlayerTokenRsp extends BasePacket { - public PacketGetPlayerTokenRsp(GameSession session) { - super(PacketOpcodes.GetPlayerTokenRsp, true); - - this.setUseDispatchKey(true); + public PacketGetPlayerTokenRsp(GameSession session) { + super(PacketOpcodes.GetPlayerTokenRsp, true); - GetPlayerTokenRsp p = GetPlayerTokenRsp.newBuilder() - .setUid(session.getPlayer().getUid()) - .setToken(session.getAccount().getToken()) - .setAccountType(1) - .setIsProficientPlayer(session.getPlayer().getAvatars().getAvatarCount() > 0) // Not sure where this goes - .setSecretKeySeed(Crypto.ENCRYPT_SEED) - .setSecurityCmdBuffer(ByteString.copyFrom(Crypto.ENCRYPT_SEED_BUFFER)) - .setPlatformType(3) - .setChannelId(1) - .setCountryCode("US") - .setClientVersionRandomKey("c25-314dd05b0b5f") - .setRegPlatform(3) - .setClientIpStr(session.getAddress().getAddress().getHostAddress()) - .build(); - - this.setData(p.toByteArray()); - } - - public PacketGetPlayerTokenRsp(GameSession session, int retcode, String msg, int blackEndTime) { - super(PacketOpcodes.GetPlayerTokenRsp, true); - - this.setUseDispatchKey(true); - - GetPlayerTokenRsp p = GetPlayerTokenRsp.newBuilder() - .setUid(session.getPlayer().getUid()) - .setIsProficientPlayer(session.getPlayer().getAvatars().getAvatarCount() > 0) - .setRetcode(retcode) - .setMsg(msg) - .setBlackUidEndTime(blackEndTime) - .setRegPlatform(3) - .setCountryCode("US") - .setClientIpStr(session.getAddress().getAddress().getHostAddress()) - .build(); - - this.setData(p.toByteArray()); - } + this.setUseDispatchKey(true); + + GetPlayerTokenRsp p = GetPlayerTokenRsp.newBuilder() + .setUid(session.getPlayer().getUid()) + .setToken(session.getAccount().getToken()) + .setAccountType(1) + .setIsProficientPlayer(session.getPlayer().getAvatars().getAvatarCount() > 0) // Not sure where this goes + .setSecretKeySeed(Crypto.ENCRYPT_SEED) + .setSecurityCmdBuffer(ByteString.copyFrom(Crypto.ENCRYPT_SEED_BUFFER)) + .setPlatformType(3) + .setChannelId(1) + .setCountryCode("US") + .setClientVersionRandomKey("c25-314dd05b0b5f") + .setRegPlatform(3) + .setClientIpStr(session.getAddress().getAddress().getHostAddress()) + .build(); + + this.setData(p.toByteArray()); + } + + public PacketGetPlayerTokenRsp(GameSession session, int retcode, String msg, int blackEndTime) { + super(PacketOpcodes.GetPlayerTokenRsp, true); + + this.setUseDispatchKey(true); + + GetPlayerTokenRsp p = GetPlayerTokenRsp.newBuilder() + .setUid(session.getPlayer().getUid()) + .setIsProficientPlayer(session.getPlayer().getAvatars().getAvatarCount() > 0) + .setRetcode(retcode) + .setMsg(msg) + .setBlackUidEndTime(blackEndTime) + .setRegPlatform(3) + .setCountryCode("US") + .setClientIpStr(session.getAddress().getAddress().getHostAddress()) + .build(); + + this.setData(p.toByteArray()); + } + + public PacketGetPlayerTokenRsp(GameSession session, String encryptedSeed, String encryptedSeedSign) { + super(PacketOpcodes.GetPlayerTokenRsp, true); + + this.setUseDispatchKey(true); + + GetPlayerTokenRsp p = GetPlayerTokenRsp.newBuilder() + .setUid(session.getPlayer().getUid()) + .setToken(session.getAccount().getToken()) + .setAccountType(1) + .setIsProficientPlayer(session.getPlayer().getAvatars().getAvatarCount() > 0) // Not sure where this goes + .setSecretKeySeed(Crypto.ENCRYPT_SEED) + .setSecurityCmdBuffer(ByteString.copyFrom(Crypto.ENCRYPT_SEED_BUFFER)) + .setPlatformType(3) + .setChannelId(1) + .setCountryCode("US") + .setClientVersionRandomKey("c25-314dd05b0b5f") + .setRegPlatform(3) + .setClientIpStr(session.getAddress().getAddress().getHostAddress()) + .setEncryptedSeed(encryptedSeed) + .setSeedSignature(encryptedSeedSign) + .build(); + + this.setData(p.toByteArray()); + } } diff --git a/src/main/java/emu/grasscutter/utils/ByteHelper.java b/src/main/java/emu/grasscutter/utils/ByteHelper.java new file mode 100644 index 000000000..1f9e408b1 --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/ByteHelper.java @@ -0,0 +1,24 @@ +package emu.grasscutter.utils; + +public class ByteHelper { + public static byte[] changeBytes(byte[] a) { + byte[] b = new byte[a.length]; + for (int i = 0; i < a.length; i++) { + b[i] = a[a.length - i - 1]; + } + return b; + } + + public static byte[] longToBytes(long x) { + byte[] bytes = new byte[8]; + bytes[0] = (byte) (x >> 56); + bytes[1] = (byte) (x >> 48); + bytes[2] = (byte) (x >> 40); + bytes[3] = (byte) (x >> 32); + bytes[4] = (byte) (x >> 24); + bytes[5] = (byte) (x >> 16); + bytes[6] = (byte) (x >> 8); + bytes[7] = (byte) (x); + return bytes; + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index 8c031c7a6..17ef6b8ca 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -112,6 +112,7 @@ public class ConfigContainer { public static class Account { public boolean autoCreate = false; + public boolean EXPERIMENTAL_RealPassword = false; public String[] defaultPermissions = {}; public int maxPlayer = -1; } @@ -210,6 +211,7 @@ public class ConfigContainer { public int cap = 160; public int rechargeTime = 480; } + public boolean uaPatchCompatible = false; } public static class JoinOptions { diff --git a/src/main/java/emu/grasscutter/utils/Crypto.java b/src/main/java/emu/grasscutter/utils/Crypto.java index 3bde63aa7..c8a264f48 100644 --- a/src/main/java/emu/grasscutter/utils/Crypto.java +++ b/src/main/java/emu/grasscutter/utils/Crypto.java @@ -1,13 +1,13 @@ package emu.grasscutter.utils; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; import java.security.SecureRandom; -import java.util.Base64; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import emu.grasscutter.Grasscutter; -import emu.grasscutter.net.proto.GetPlayerTokenRspOuterClass.GetPlayerTokenRsp; -import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; - -import static emu.grasscutter.Configuration.*; public final class Crypto { private static final SecureRandom secureRandom = new SecureRandom(); @@ -18,15 +18,34 @@ public final class Crypto { public static byte[] ENCRYPT_KEY; public static long ENCRYPT_SEED = Long.parseUnsignedLong("11468049314633205968"); public static byte[] ENCRYPT_SEED_BUFFER = new byte[0]; - + + public static PublicKey CUR_OS_ENCRYPT_KEY; + public static PublicKey CUR_CN_ENCRYPT_KEY; + public static PrivateKey CUR_SIGNING_KEY; + public static void loadKeys() { DISPATCH_KEY = FileUtils.readResource("/keys/dispatchKey.bin"); DISPATCH_SEED = FileUtils.readResource("/keys/dispatchSeed.bin"); ENCRYPT_KEY = FileUtils.readResource("/keys/secretKey.bin"); ENCRYPT_SEED_BUFFER = FileUtils.readResource("/keys/secretKeyBuffer.bin"); + + try { + //These should be loaded from ChannelConfig_whatever.json + CUR_SIGNING_KEY = KeyFactory.getInstance("RSA") + .generatePrivate(new PKCS8EncodedKeySpec(FileUtils.readResource("/keys/SigningKey.der"))); + + CUR_OS_ENCRYPT_KEY = KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(FileUtils.readResource("/keys/OSCB_Pub.der"))); + + CUR_CN_ENCRYPT_KEY = KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(FileUtils.readResource("/keys/OSCN_Pub.der"))); + } + catch (Exception e) { + Grasscutter.getLogger().error("An error occurred while loading keys.", e); + } } - + public static void xor(byte[] packet, byte[] key) { try { for (int i = 0; i < packet.length; i++) { @@ -36,7 +55,7 @@ public final class Crypto { Grasscutter.getLogger().error("Crypto error.", e); } } - + public static byte[] createSessionKey(int length) { byte[] bytes = new byte[length]; secureRandom.nextBytes(bytes); diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 6dc5a0c43..0e6c45415 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -28,6 +28,8 @@ "login_token_attempt": "[Dispatch] Client %s is trying to log in via token.", "login_token_error": "[Dispatch] Client %s failed to log in via token.", "login_token_success": "[Dispatch] Client %s logged in via token as %s.", + "login_password_error": "[Dispatch] Client %s failed to log in via password.", + "login_password_storage_error": "[Dispatch] Client %s failed to log in via password because there is no password in the database.", "combo_token_success": "[Dispatch] Client %s succeed to exchange combo token.", "combo_token_error": "[Dispatch] Client %s failed to exchange combo token.", "account_login_create_success": "[Dispatch] Client %s failed to log in: Account %s created.", @@ -37,6 +39,9 @@ "session_key_error": "Wrong session key.", "username_error": "Username not found.", "username_create_error": "Username not found, create failed.", + "password_error": "Invalid Password", + "password_length_error": "Password length must be greater then or equal to 8", + "password_storage_error": "You don't have a password for your account. Please contact an administrator.", "server_max_player_limit": "The number of online players has reached the limit" }, "router_error": "[Dispatch] Unable to attach router."