You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
@@ -110,22 +110,26 @@ CREATE TABLE IF NOT EXISTS `room_templates_items` (
|
||||
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_remember_tokens` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`token_hash` char(64) NOT NULL,
|
||||
`created_at` int(11) NOT NULL,
|
||||
`expires_at` int(11) NOT NULL,
|
||||
`ip_address` varchar(45) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `token_hash` (`token_hash`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CREATE TABLE IF NOT EXISTS `users_remember_families` (
|
||||
`family_id` char(36) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`current_version` int(11) NOT NULL DEFAULT 1,
|
||||
`created_at` int(11) NOT NULL,
|
||||
`expires_at` int(11) NOT NULL,
|
||||
`revoked` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`last_ip` varchar(45) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`family_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `expires_at` (`expires_at`),
|
||||
CONSTRAINT `fk_remember_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
CONSTRAINT `fk_remember_family_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- Optional: configure how long a remember token is valid (default 30 days).
|
||||
DROP TABLE IF EXISTS `users_remember_tokens`;
|
||||
|
||||
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('login.remember.duration.days', '30')
|
||||
('login.remember.duration.days', '30'),
|
||||
('login.remember.rotate.interval.minutes', '15'),
|
||||
('login.remember.jwt.secret', '')
|
||||
ON DUPLICATE KEY UPDATE `value` = `value`;
|
||||
|
||||
|
||||
@@ -10,4 +10,6 @@ public class GameServerAttributes {
|
||||
public static final AttributeKey<HabboRC4> CRYPTO_CLIENT = AttributeKey.valueOf("CryptoClient");
|
||||
public static final AttributeKey<HabboRC4> CRYPTO_SERVER = AttributeKey.valueOf("CryptoServer");
|
||||
public static final AttributeKey<String> WS_IP = AttributeKey.valueOf("WebSocketIP");
|
||||
public static final AttributeKey<byte[]> WS_AES_KEY = AttributeKey.valueOf("WsAesKey");
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -1,8 +1,10 @@
|
||||
package com.eu.habbo.networking.gameserver;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.messages.PacketManager;
|
||||
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
|
||||
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
|
||||
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
|
||||
import com.eu.habbo.networking.gameserver.decoders.*;
|
||||
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder;
|
||||
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageLogger;
|
||||
@@ -53,6 +55,11 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
|
||||
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
|
||||
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
|
||||
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
|
||||
|
||||
if (Emulator.getConfig().getBoolean("crypto.ws.enabled", false)) {
|
||||
ch.pipeline().addLast(WsHandshakeHandler.HANDLER_NAME, new WsHandshakeHandler());
|
||||
}
|
||||
|
||||
ch.pipeline().addLast(new GamePolicyDecoder());
|
||||
ch.pipeline().addLast(new GameByteFrameDecoder());
|
||||
ch.pipeline().addLast(new GameByteDecoder());
|
||||
|
||||
+41
-153
@@ -34,6 +34,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final String CHECK_USERNAME_PATH = "/api/auth/check-username";
|
||||
private static final String ROOM_TEMPLATES_PATH = "/api/auth/room-templates";
|
||||
private static final String REMEMBER_PATH = "/api/auth/remember";
|
||||
private static final String REFRESH_PATH = "/api/auth/refresh";
|
||||
private static final String HEALTH_PATH = "/api/health";
|
||||
|
||||
private static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$");
|
||||
@@ -56,6 +57,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
&& !path.equals(CHECK_EMAIL_PATH) && !path.equals(CHECK_USERNAME_PATH)
|
||||
&& !path.equals(ROOM_TEMPLATES_PATH)
|
||||
&& !path.equals(REMEMBER_PATH)
|
||||
&& !path.equals(REFRESH_PATH)
|
||||
&& !path.equals(HEALTH_PATH)) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
@@ -139,6 +141,10 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
handleRemember(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
if (path.equals(REFRESH_PATH)) {
|
||||
handleRefresh(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
|
||||
String turnstileToken = readString(body, "turnstileToken");
|
||||
if (!TurnstileVerifier.verify(turnstileToken, ip)) {
|
||||
@@ -154,8 +160,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Availability probes ─── */
|
||||
|
||||
private void handleCheckEmail(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
if (!AuthRateLimiter.tryProbe(ip)) {
|
||||
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
|
||||
@@ -235,8 +239,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, res);
|
||||
}
|
||||
|
||||
/* ─── Logout ─── */
|
||||
|
||||
private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) {
|
||||
String ssoTicket = readString(body, "ssoTicket");
|
||||
String rememberToken = readString(body, "rememberToken").trim();
|
||||
@@ -273,17 +275,8 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete only the specific remember token for this device.
|
||||
// Other devices keep their tokens and can still silent-login.
|
||||
if (!rememberToken.isEmpty()) {
|
||||
String hash = sha256Hex(rememberToken);
|
||||
if (hash != null) {
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE token_hash = ?")) {
|
||||
del.setString(1, hash);
|
||||
del.executeUpdate();
|
||||
}
|
||||
}
|
||||
RememberJwtService.revokeFromToken(conn, rememberToken);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Logout cleanup failed", e);
|
||||
@@ -292,69 +285,16 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
}
|
||||
|
||||
/* ─── Remember me ─── */
|
||||
|
||||
private void handleRemember(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String rememberToken = readString(body, "rememberToken").trim();
|
||||
if (rememberToken.isEmpty()) {
|
||||
String jwt = readString(body, "rememberToken").trim();
|
||||
if (jwt.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing rememberToken."));
|
||||
return;
|
||||
}
|
||||
|
||||
String hash = sha256Hex(rememberToken);
|
||||
if (hash == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
return;
|
||||
}
|
||||
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
int userId = 0;
|
||||
int tokenRowId = 0;
|
||||
|
||||
try (PreparedStatement sel = conn.prepareStatement(
|
||||
"SELECT id, user_id, expires_at FROM users_remember_tokens WHERE token_hash = ? LIMIT 1")) {
|
||||
sel.setString(1, hash);
|
||||
try (ResultSet rs = sel.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
if (rs.getInt("expires_at") > now) {
|
||||
userId = rs.getInt("user_id");
|
||||
tokenRowId = rs.getInt("id");
|
||||
} else {
|
||||
tokenRowId = rs.getInt("id"); // expired - still purge below
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userId <= 0) {
|
||||
if (tokenRowId > 0) {
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE id = ?")) {
|
||||
del.setInt(1, tokenRowId);
|
||||
del.executeUpdate();
|
||||
}
|
||||
}
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
|
||||
return;
|
||||
}
|
||||
|
||||
String username = null;
|
||||
try (PreparedStatement usr = conn.prepareStatement(
|
||||
"SELECT username FROM users WHERE id = ? LIMIT 1")) {
|
||||
usr.setInt(1, userId);
|
||||
try (ResultSet rs = usr.executeQuery()) {
|
||||
if (rs.next()) username = rs.getString("username");
|
||||
}
|
||||
}
|
||||
|
||||
if (username == null) {
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE id = ?")) {
|
||||
del.setInt(1, tokenRowId);
|
||||
del.executeUpdate();
|
||||
}
|
||||
RememberJwtService.RotationResult rot = RememberJwtService.rotate(conn, jwt, ip);
|
||||
if (rot == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
|
||||
return;
|
||||
}
|
||||
@@ -364,23 +304,15 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
"UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) {
|
||||
upd.setString(1, ssoTicket);
|
||||
upd.setString(2, ip == null ? "" : ip);
|
||||
upd.setInt(3, userId);
|
||||
upd.setInt(3, rot.userId);
|
||||
upd.executeUpdate();
|
||||
}
|
||||
|
||||
// Rotate: drop the consumed token and issue a new one.
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE id = ?")) {
|
||||
del.setInt(1, tokenRowId);
|
||||
del.executeUpdate();
|
||||
}
|
||||
|
||||
String newToken = issueRememberToken(conn, userId, ip);
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("ssoTicket", ssoTicket);
|
||||
ok.addProperty("username", username);
|
||||
if (newToken != null) ok.addProperty("rememberToken", newToken);
|
||||
ok.addProperty("username", rot.username);
|
||||
ok.addProperty("rememberToken", rot.jwt);
|
||||
ok.addProperty("expiresAt", rot.expiresAt);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Remember login failed", e);
|
||||
@@ -388,57 +320,29 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a fresh remember-me token for a user, stores the hash,
|
||||
* and returns the raw base64url string to embed in the response.
|
||||
* Returns null on failure (the login still succeeds).
|
||||
*/
|
||||
private static String issueRememberToken(Connection conn, int userId, String ip) {
|
||||
byte[] buf = new byte[32];
|
||||
RNG.nextBytes(buf);
|
||||
String raw = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
String hash = sha256Hex(raw);
|
||||
if (hash == null) return null;
|
||||
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
int days = Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30));
|
||||
int expiresAt = now + (days * 24 * 60 * 60);
|
||||
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO users_remember_tokens (user_id, token_hash, created_at, expires_at, ip_address) VALUES (?, ?, ?, ?, ?)")) {
|
||||
ins.setInt(1, userId);
|
||||
ins.setString(2, hash);
|
||||
ins.setInt(3, now);
|
||||
ins.setInt(4, expiresAt);
|
||||
ins.setString(5, ip == null ? "" : ip);
|
||||
ins.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to persist remember token for userId=" + userId, e);
|
||||
return null;
|
||||
private void handleRefresh(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String jwt = readString(body, "rememberToken").trim();
|
||||
if (jwt.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing rememberToken."));
|
||||
return;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static String sha256Hex(String input) {
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder(digest.length * 2);
|
||||
for (byte b : digest) {
|
||||
String h = Integer.toHexString(b & 0xff);
|
||||
if (h.length() == 1) sb.append('0');
|
||||
sb.append(h);
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
RememberJwtService.RotationResult rot = RememberJwtService.rotate(conn, jwt, ip);
|
||||
if (rot == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
|
||||
return;
|
||||
}
|
||||
return sb.toString();
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("rememberToken", rot.jwt);
|
||||
ok.addProperty("expiresAt", rot.expiresAt);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("sha256Hex failed", e);
|
||||
return null;
|
||||
LOGGER.error("Refresh failed", e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Login ─── */
|
||||
|
||||
private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String username = readString(body, "username").trim();
|
||||
String password = readString(body, "password");
|
||||
@@ -487,7 +391,16 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
upd.executeUpdate();
|
||||
}
|
||||
|
||||
String rememberToken = rememberMe ? issueRememberToken(conn, userId, ip) : null;
|
||||
String rememberToken = null;
|
||||
if (rememberMe) {
|
||||
try {
|
||||
RememberJwtService.RotationResult issued = RememberJwtService.issueForNewFamily(
|
||||
conn, userId, rs.getString("username"), ip);
|
||||
rememberToken = issued.jwt;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to issue remember-me JWT for userId=" + userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
AuthRateLimiter.recordSuccess(ip);
|
||||
|
||||
@@ -503,8 +416,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Register ─── */
|
||||
|
||||
private void handleRegister(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
if (!Emulator.getConfig().getBoolean("login.register.enabled", true)) {
|
||||
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, errorPayload("Registration is closed."));
|
||||
@@ -633,13 +544,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the template carries a custom heightmap (override_model='1' and a
|
||||
* non-empty heightmap), creates the matching room_models_custom row keyed
|
||||
* by the new room id and renames the room's model to custom_<newRoomId>.
|
||||
* Without this, cloned rooms reference a layout that doesn't exist
|
||||
* (the source room's id) and load as a black screen.
|
||||
*/
|
||||
private static void materializeCustomLayout(Connection conn, int templateId, int newRoomId) {
|
||||
String overrideModel = "0";
|
||||
String heightmap = "";
|
||||
@@ -697,11 +601,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
LOGGER.info("[auth/register] materialized custom layout '{}' for roomId={}", customName, newRoomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds starting balances into users_currency for duckets (type=0) and
|
||||
* diamonds (type=5). Only inserts when the amount is > 0. Credits live
|
||||
* in users.credits and are set directly during the register INSERT.
|
||||
*/
|
||||
private static void seedUserCurrencies(Connection conn, int userId, int duckets, int diamonds) {
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " +
|
||||
@@ -725,8 +624,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Room templates (registration step 3) ─── */
|
||||
|
||||
private void handleRoomTemplates(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
JsonArray templates = new JsonArray();
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
@@ -754,11 +651,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones a room_templates entry + its room_templates_items into the new
|
||||
* user's rooms/items rows, then points their home_room at the new room.
|
||||
* Failures here do not abort registration; the account is still created.
|
||||
*/
|
||||
private static void cloneTemplateForUser(Connection conn, int templateId, int userId, String userName) {
|
||||
LOGGER.info("[auth/register] cloning template id={} for user id={} name='{}'", templateId, userId, userName);
|
||||
|
||||
@@ -836,8 +728,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Forgot password ─── */
|
||||
|
||||
private void handleForgot(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String email = readString(body, "email").trim();
|
||||
|
||||
@@ -891,8 +781,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
}
|
||||
|
||||
/* ─── Helpers ─── */
|
||||
|
||||
private static boolean checkPassword(String plain, String stored) {
|
||||
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
|
||||
try {
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class RememberJwtService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RememberJwtService.class);
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
private static final Base64.Encoder URL_ENC = Base64.getUrlEncoder().withoutPadding();
|
||||
private static final Base64.Decoder URL_DEC = Base64.getUrlDecoder();
|
||||
|
||||
private static volatile String cachedSecret = null;
|
||||
|
||||
private RememberJwtService() {}
|
||||
|
||||
public static final class RotationResult {
|
||||
public final String jwt;
|
||||
public final int userId;
|
||||
public final String username;
|
||||
public final long expiresAt;
|
||||
|
||||
RotationResult(String jwt, int userId, String username, long expiresAt) {
|
||||
this.jwt = jwt;
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
private static int familyTtlDays() {
|
||||
return Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30));
|
||||
}
|
||||
|
||||
private static long familyTtlSeconds() {
|
||||
return familyTtlDays() * 86400L;
|
||||
}
|
||||
|
||||
private static String secret() {
|
||||
String s = cachedSecret;
|
||||
if (s != null && !s.isEmpty()) return s;
|
||||
|
||||
synchronized (RememberJwtService.class) {
|
||||
if (cachedSecret != null && !cachedSecret.isEmpty()) return cachedSecret;
|
||||
|
||||
String configured = Emulator.getConfig().getValue("login.remember.jwt.secret", "");
|
||||
if (configured != null && !configured.isEmpty()) {
|
||||
cachedSecret = configured;
|
||||
return configured;
|
||||
}
|
||||
|
||||
byte[] buf = new byte[48];
|
||||
RNG.nextBytes(buf);
|
||||
String generated = Base64.getEncoder().withoutPadding().encodeToString(buf);
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"INSERT INTO emulator_settings (`key`, `value`) VALUES ('login.remember.jwt.secret', ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
|
||||
stmt.setString(1, generated);
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Could not persist generated login.remember.jwt.secret; using in-memory only", e);
|
||||
}
|
||||
|
||||
Emulator.getConfig().update("login.remember.jwt.secret", generated);
|
||||
cachedSecret = generated;
|
||||
LOGGER.info("[auth/remember] generated new JWT signing secret (persisted to emulator_settings)");
|
||||
return generated;
|
||||
}
|
||||
}
|
||||
|
||||
public static RotationResult issueForNewFamily(Connection conn, int userId, String username, String ip) throws SQLException {
|
||||
String familyId = UUID.randomUUID().toString();
|
||||
long now = Emulator.getIntUnixTimestamp();
|
||||
long expiresAt = now + familyTtlSeconds();
|
||||
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO users_remember_families (family_id, user_id, current_version, created_at, expires_at, revoked, last_ip) "
|
||||
+ "VALUES (?, ?, 1, ?, ?, 0, ?)")) {
|
||||
ins.setString(1, familyId);
|
||||
ins.setInt(2, userId);
|
||||
ins.setLong(3, now);
|
||||
ins.setLong(4, expiresAt);
|
||||
ins.setString(5, ip == null ? "" : ip);
|
||||
ins.executeUpdate();
|
||||
}
|
||||
|
||||
String jwt = buildJwt(userId, familyId, 1, now, expiresAt);
|
||||
return new RotationResult(jwt, userId, username, expiresAt);
|
||||
}
|
||||
|
||||
public static RotationResult rotate(Connection conn, String jwt, String ip) {
|
||||
ParsedJwt parsed;
|
||||
try {
|
||||
parsed = verifyAndParse(jwt);
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("[auth/remember] invalid JWT: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
long now = Emulator.getIntUnixTimestamp();
|
||||
if (parsed.exp <= now) return null;
|
||||
|
||||
int familyVersion = 0;
|
||||
boolean revoked = false;
|
||||
long familyExpiresAt = 0;
|
||||
try (PreparedStatement sel = conn.prepareStatement(
|
||||
"SELECT current_version, revoked, expires_at FROM users_remember_families WHERE family_id = ? AND user_id = ? LIMIT 1")) {
|
||||
sel.setString(1, parsed.familyId);
|
||||
sel.setInt(2, parsed.userId);
|
||||
try (ResultSet rs = sel.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
familyVersion = rs.getInt("current_version");
|
||||
revoked = rs.getInt("revoked") != 0;
|
||||
familyExpiresAt = rs.getLong("expires_at");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] family lookup failed", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (revoked || familyExpiresAt <= now) return null;
|
||||
|
||||
if (parsed.version < familyVersion) {
|
||||
LOGGER.warn("[auth/remember] replay detected: familyId={} presented v={} but current is v={}, revoking family",
|
||||
parsed.familyId, parsed.version, familyVersion);
|
||||
revokeFamilyById(conn, parsed.familyId);
|
||||
return null;
|
||||
}
|
||||
if (parsed.version > familyVersion) {
|
||||
LOGGER.warn("[auth/remember] future version: familyId={} presented v={} but current is v={}",
|
||||
parsed.familyId, parsed.version, familyVersion);
|
||||
return null;
|
||||
}
|
||||
|
||||
int newVersion = familyVersion + 1;
|
||||
long newExpiresAt = now + familyTtlSeconds();
|
||||
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users_remember_families SET current_version = ?, expires_at = ?, last_ip = ? "
|
||||
+ "WHERE family_id = ? AND current_version = ? AND revoked = 0")) {
|
||||
upd.setInt(1, newVersion);
|
||||
upd.setLong(2, newExpiresAt);
|
||||
upd.setString(3, ip == null ? "" : ip);
|
||||
upd.setString(4, parsed.familyId);
|
||||
upd.setInt(5, familyVersion);
|
||||
int rows = upd.executeUpdate();
|
||||
if (rows == 0) return null;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] rotation update failed", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
String username = null;
|
||||
try (PreparedStatement usr = conn.prepareStatement("SELECT username FROM users WHERE id = ? LIMIT 1")) {
|
||||
usr.setInt(1, parsed.userId);
|
||||
try (ResultSet rs = usr.executeQuery()) {
|
||||
if (rs.next()) username = rs.getString("username");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] username lookup failed", e);
|
||||
}
|
||||
|
||||
if (username == null) return null;
|
||||
|
||||
String newJwt = buildJwt(parsed.userId, parsed.familyId, newVersion, now, newExpiresAt);
|
||||
return new RotationResult(newJwt, parsed.userId, username, newExpiresAt);
|
||||
}
|
||||
|
||||
public static void revokeFromToken(Connection conn, String jwt) {
|
||||
try {
|
||||
ParsedJwt p = verifyAndParse(jwt);
|
||||
revokeFamilyById(conn, p.familyId);
|
||||
} catch (Exception ignored) { }
|
||||
}
|
||||
|
||||
private static void revokeFamilyById(Connection conn, String familyId) {
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users_remember_families SET revoked = 1 WHERE family_id = ?")) {
|
||||
upd.setString(1, familyId);
|
||||
upd.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] revoke failed for familyId=" + familyId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildJwt(int userId, String familyId, int version, long iat, long exp) {
|
||||
JsonObject header = new JsonObject();
|
||||
header.addProperty("alg", "HS256");
|
||||
header.addProperty("typ", "JWT");
|
||||
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("sub", userId);
|
||||
payload.addProperty("fid", familyId);
|
||||
payload.addProperty("v", version);
|
||||
payload.addProperty("iat", iat);
|
||||
payload.addProperty("exp", exp);
|
||||
payload.addProperty("typ", "refresh");
|
||||
|
||||
String h = URL_ENC.encodeToString(header.toString().getBytes(StandardCharsets.UTF_8));
|
||||
String p = URL_ENC.encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8));
|
||||
String signingInput = h + "." + p;
|
||||
String sig = URL_ENC.encodeToString(hmacSha256(secret().getBytes(StandardCharsets.UTF_8),
|
||||
signingInput.getBytes(StandardCharsets.UTF_8)));
|
||||
return signingInput + "." + sig;
|
||||
}
|
||||
|
||||
private static final class ParsedJwt {
|
||||
final int userId;
|
||||
final String familyId;
|
||||
final int version;
|
||||
final long exp;
|
||||
|
||||
ParsedJwt(int userId, String familyId, int version, long exp) {
|
||||
this.userId = userId;
|
||||
this.familyId = familyId;
|
||||
this.version = version;
|
||||
this.exp = exp;
|
||||
}
|
||||
}
|
||||
|
||||
private static ParsedJwt verifyAndParse(String jwt) throws Exception {
|
||||
if (jwt == null || jwt.isEmpty()) throw new IllegalArgumentException("empty");
|
||||
|
||||
String[] parts = jwt.split("\\.");
|
||||
if (parts.length != 3) throw new IllegalArgumentException("not 3 segments");
|
||||
|
||||
String signingInput = parts[0] + "." + parts[1];
|
||||
byte[] expected = hmacSha256(secret().getBytes(StandardCharsets.UTF_8), signingInput.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] provided = URL_DEC.decode(parts[2]);
|
||||
if (!constantTimeEquals(expected, provided)) throw new SecurityException("bad signature");
|
||||
|
||||
byte[] payloadBytes = URL_DEC.decode(parts[1]);
|
||||
JsonObject payload = JsonParser.parseString(new String(payloadBytes, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
|
||||
if (!payload.has("typ") || !"refresh".equals(payload.get("typ").getAsString())) throw new IllegalArgumentException("wrong typ");
|
||||
int userId = payload.get("sub").getAsInt();
|
||||
String fid = payload.get("fid").getAsString();
|
||||
int version = payload.get("v").getAsInt();
|
||||
long exp = payload.get("exp").getAsLong();
|
||||
|
||||
return new ParsedJwt(userId, fid, version, exp);
|
||||
}
|
||||
|
||||
private static byte[] hmacSha256(byte[] key, byte[] data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
return mac.doFinal(data);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("HmacSHA256 unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean constantTimeEquals(byte[] a, byte[] b) {
|
||||
if (a == null || b == null || a.length != b.length) return false;
|
||||
int r = 0;
|
||||
for (int i = 0; i < a.length; i++) r |= a[i] ^ b[i];
|
||||
return r == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WsAesDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WsAesDecoder.class);
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
|
||||
byte[] key = ctx.channel().attr(GameServerAttributes.WS_AES_KEY).get();
|
||||
if (key == null) {
|
||||
LOGGER.warn("[ws-crypto] inbound frame with no session key, closing");
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int readable = in.readableBytes();
|
||||
if (readable < WsSessionCrypto.NONCE_LEN + 16) {
|
||||
LOGGER.warn("[ws-crypto] inbound frame too short ({} bytes)", readable);
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] nonce = new byte[WsSessionCrypto.NONCE_LEN];
|
||||
in.readBytes(nonce);
|
||||
|
||||
byte[] ct = new byte[in.readableBytes()];
|
||||
in.readBytes(ct);
|
||||
|
||||
try {
|
||||
byte[] plain = WsSessionCrypto.aesGcmDecrypt(key, nonce, ct);
|
||||
out.add(Unpooled.wrappedBuffer(plain));
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("[ws-crypto] AES-GCM decrypt failed", e);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WsAesEncoder extends MessageToMessageEncoder<ByteBuf> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WsAesEncoder.class);
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
|
||||
byte[] key = ctx.channel().attr(GameServerAttributes.WS_AES_KEY).get();
|
||||
if (key == null) {
|
||||
LOGGER.warn("[ws-crypto] outbound frame with no session key, dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] plain = new byte[in.readableBytes()];
|
||||
in.readBytes(plain);
|
||||
|
||||
byte[] nonce = WsSessionCrypto.randomNonce();
|
||||
byte[] ct = WsSessionCrypto.aesGcmEncrypt(key, nonce, plain);
|
||||
|
||||
ByteBuf framed = ctx.alloc().buffer(nonce.length + ct.length);
|
||||
framed.writeBytes(nonce);
|
||||
framed.writeBytes(ct);
|
||||
out.add(framed);
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
public class WsHandshakeHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WsHandshakeHandler.class);
|
||||
|
||||
public static final String HANDLER_NAME = "wsCryptoHandshake";
|
||||
|
||||
private KeyPair serverKeyPair;
|
||||
private boolean helloSent = false;
|
||||
private boolean handshakeComplete = false;
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
|
||||
sendServerHello(ctx);
|
||||
}
|
||||
super.userEventTriggered(ctx, evt);
|
||||
}
|
||||
|
||||
private void sendServerHello(ChannelHandlerContext ctx) {
|
||||
if (helloSent) return;
|
||||
try {
|
||||
this.serverKeyPair = WsSessionCrypto.generateEphemeralKeyPair();
|
||||
byte[] spki = WsSessionCrypto.encodePublicKeySpki(serverKeyPair.getPublic());
|
||||
|
||||
ByteBuf buf = ctx.alloc().buffer(4 + 1 + 2 + spki.length);
|
||||
buf.writeInt(WsSessionCrypto.HANDSHAKE_MAGIC);
|
||||
buf.writeByte(WsSessionCrypto.TYPE_SERVER_HELLO);
|
||||
buf.writeShort(spki.length);
|
||||
buf.writeBytes(spki);
|
||||
|
||||
ctx.writeAndFlush(buf);
|
||||
helloSent = true;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] failed to send server_hello", e);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (handshakeComplete) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(msg instanceof ByteBuf)) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuf in = (ByteBuf) msg;
|
||||
try {
|
||||
if (in.readableBytes() < 7) {
|
||||
LOGGER.warn("[ws-crypto] handshake frame too short ({} bytes) from {}", in.readableBytes(), clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int magic = in.readInt();
|
||||
if (magic != WsSessionCrypto.HANDSHAKE_MAGIC) {
|
||||
LOGGER.warn("[ws-crypto] handshake magic mismatch: 0x{} from {}", Integer.toHexString(magic), clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
byte type = in.readByte();
|
||||
if (type != WsSessionCrypto.TYPE_CLIENT_HELLO) {
|
||||
LOGGER.warn("[ws-crypto] expected client_hello, got type=0x{} from {}", Integer.toHexString(type & 0xff), clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int keyLen = in.readUnsignedShort();
|
||||
if (keyLen <= 0 || keyLen > in.readableBytes() || keyLen > 2048) {
|
||||
LOGGER.warn("[ws-crypto] invalid client key length {} from {}", keyLen, clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] clientSpki = new byte[keyLen];
|
||||
in.readBytes(clientSpki);
|
||||
|
||||
PublicKey clientPub = WsSessionCrypto.decodePublicKeySpki(clientSpki);
|
||||
PrivateKey ourPriv = serverKeyPair.getPrivate();
|
||||
byte[] shared = WsSessionCrypto.deriveSharedSecret(ourPriv, clientPub);
|
||||
byte[] aesKey = WsSessionCrypto.deriveAesKey(shared);
|
||||
|
||||
ctx.channel().attr(GameServerAttributes.WS_AES_KEY).set(aesKey);
|
||||
|
||||
ChannelPipeline p = ctx.pipeline();
|
||||
p.addAfter(HANDLER_NAME, "wsAesDecoder", new WsAesDecoder());
|
||||
p.addAfter(HANDLER_NAME, "wsAesEncoder", new WsAesEncoder());
|
||||
handshakeComplete = true;
|
||||
p.remove(this);
|
||||
|
||||
LOGGER.debug("[ws-crypto] handshake complete for {}", clientAddress(ctx));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] handshake failed from " + clientAddress(ctx), e);
|
||||
ctx.close();
|
||||
} finally {
|
||||
in.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static String clientAddress(ChannelHandlerContext ctx) {
|
||||
String wsIp = ctx.channel().attr(GameServerAttributes.WS_IP).get();
|
||||
if (wsIp != null && !wsIp.isEmpty()) return wsIp;
|
||||
return String.valueOf(ctx.channel().remoteAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
LOGGER.error("[ws-crypto] handshake handler error", cause);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyAgreement;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class WsSessionCrypto {
|
||||
|
||||
public static final int HANDSHAKE_MAGIC = 0xC0DEC0DE;
|
||||
public static final byte TYPE_SERVER_HELLO = 0x01;
|
||||
public static final byte TYPE_CLIENT_HELLO = 0x02;
|
||||
|
||||
public static final String HKDF_INFO = "nitro-ws-v1";
|
||||
public static final int AES_KEY_LEN = 32;
|
||||
public static final int NONCE_LEN = 12;
|
||||
public static final int GCM_TAG_BITS = 128;
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
|
||||
private WsSessionCrypto() {}
|
||||
|
||||
public static KeyPair generateEphemeralKeyPair() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
|
||||
kpg.initialize(new java.security.spec.ECGenParameterSpec("secp256r1"), RNG);
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
public static byte[] encodePublicKeySpki(PublicKey publicKey) {
|
||||
return publicKey.getEncoded();
|
||||
}
|
||||
|
||||
public static PublicKey decodePublicKeySpki(byte[] spki) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spki));
|
||||
}
|
||||
|
||||
public static byte[] deriveSharedSecret(PrivateKey ourPrivate, PublicKey theirPublic) throws GeneralSecurityException {
|
||||
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
|
||||
ka.init(ourPrivate);
|
||||
ka.doPhase(theirPublic, true);
|
||||
return ka.generateSecret();
|
||||
}
|
||||
|
||||
public static byte[] hkdfSha256(byte[] ikm, byte[] salt, byte[] info, int outLen) throws GeneralSecurityException {
|
||||
if (salt == null || salt.length == 0) salt = new byte[32];
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(salt, "HmacSHA256"));
|
||||
byte[] prk = mac.doFinal(ikm);
|
||||
|
||||
int hashLen = 32;
|
||||
int n = (outLen + hashLen - 1) / hashLen;
|
||||
if (n > 255) throw new GeneralSecurityException("HKDF output too long");
|
||||
|
||||
ByteArrayOutputStream okm = new ByteArrayOutputStream();
|
||||
byte[] t = new byte[0];
|
||||
|
||||
for (int i = 1; i <= n; i++) {
|
||||
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
|
||||
mac.update(t);
|
||||
if (info != null) mac.update(info);
|
||||
mac.update((byte) i);
|
||||
t = mac.doFinal();
|
||||
okm.write(t, 0, t.length);
|
||||
}
|
||||
|
||||
byte[] result = okm.toByteArray();
|
||||
return (result.length == outLen) ? result : Arrays.copyOf(result, outLen);
|
||||
}
|
||||
|
||||
public static byte[] deriveAesKey(byte[] sharedSecret) throws GeneralSecurityException {
|
||||
return hkdfSha256(sharedSecret, null, HKDF_INFO.getBytes(StandardCharsets.UTF_8), AES_KEY_LEN);
|
||||
}
|
||||
|
||||
public static byte[] aesGcmEncrypt(byte[] key, byte[] nonce, byte[] plaintext) throws GeneralSecurityException {
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
|
||||
return c.doFinal(plaintext);
|
||||
}
|
||||
|
||||
public static byte[] aesGcmDecrypt(byte[] key, byte[] nonce, byte[] ciphertextWithTag) throws GeneralSecurityException {
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
|
||||
return c.doFinal(ciphertextWithTag);
|
||||
}
|
||||
|
||||
public static byte[] randomNonce() {
|
||||
byte[] n = new byte[NONCE_LEN];
|
||||
RNG.nextBytes(n);
|
||||
return n;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -8,6 +8,9 @@ db.params=
|
||||
db.pool.minsize=25
|
||||
db.pool.maxsize=100
|
||||
|
||||
# Encrypt your traffic
|
||||
crypto.ws.enabled=0
|
||||
|
||||
#Game Configuration.
|
||||
#Host IP. Most likely just 0.0.0.0 Use 127.0.0.1 if you want to play on LAN.
|
||||
game.host=0.0.0.0
|
||||
|
||||
Reference in New Issue
Block a user