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
🆙 CryptoV2 - please red the how_things_work on DC !!!
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('crypto.ws.enabled', '0'),
|
||||
('crypto.ws.signing.enabled', '0'),
|
||||
('crypto.ws.signing.public_key', ''),
|
||||
('crypto.ws.signing.private_key', '')
|
||||
ON DUPLICATE KEY UPDATE `value` = `value`;
|
||||
|
||||
|
||||
@@ -96,6 +96,16 @@ public class GameServer extends Server {
|
||||
LOGGER.error("Failed to start WebSocket server on {}:{}", wsHost, wsPort);
|
||||
} else {
|
||||
LOGGER.info("WebSocket server started on {}:{} (SSL: {})", wsHost, wsPort, wsInitializer.isSslEnabled());
|
||||
|
||||
if (com.eu.habbo.Emulator.getConfig().getBoolean("crypto.ws.signing.enabled", false)) {
|
||||
try {
|
||||
com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.get();
|
||||
LOGGER.info("[ws-crypto] signing public key ready: {}",
|
||||
com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.publicKeyBase64());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] failed to warm signing keypair", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
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 SERVER_KEY_PATH = "/api/auth/server-key";
|
||||
private static final String HEALTH_PATH = "/api/health";
|
||||
|
||||
private static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$");
|
||||
@@ -58,6 +59,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
&& !path.equals(ROOM_TEMPLATES_PATH)
|
||||
&& !path.equals(REMEMBER_PATH)
|
||||
&& !path.equals(REFRESH_PATH)
|
||||
&& !path.equals(SERVER_KEY_PATH)
|
||||
&& !path.equals(HEALTH_PATH)) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
@@ -96,6 +98,15 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.equals(SERVER_KEY_PATH)) {
|
||||
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
|
||||
return;
|
||||
}
|
||||
handleServerKey(ctx, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method() != HttpMethod.POST) {
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use POST."));
|
||||
return;
|
||||
@@ -651,6 +662,18 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, res);
|
||||
}
|
||||
|
||||
private void handleServerKey(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
try {
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("publicKey", com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.publicKeyBase64());
|
||||
ok.addProperty("algorithm", "ECDSA-P256-SHA256");
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("server-key fetch failed", e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.util.Base64;
|
||||
|
||||
public final class CryptoSigningKeyManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CryptoSigningKeyManager.class);
|
||||
private static final String KEY_PUBLIC = "crypto.ws.signing.public_key";
|
||||
private static final String KEY_PRIVATE = "crypto.ws.signing.private_key";
|
||||
private static volatile KeyPair cached;
|
||||
private static volatile String cachedPublicB64;
|
||||
private CryptoSigningKeyManager() {}
|
||||
|
||||
public static KeyPair get() {
|
||||
KeyPair kp = cached;
|
||||
if (kp != null) return kp;
|
||||
|
||||
synchronized (CryptoSigningKeyManager.class) {
|
||||
if (cached != null) return cached;
|
||||
|
||||
String pubB64 = Emulator.getConfig().getValue(KEY_PUBLIC, "");
|
||||
String privB64 = Emulator.getConfig().getValue(KEY_PRIVATE, "");
|
||||
|
||||
if (pubB64 != null && !pubB64.isEmpty() && privB64 != null && !privB64.isEmpty()) {
|
||||
try {
|
||||
byte[] pubDer = Base64.getDecoder().decode(pubB64);
|
||||
byte[] privDer = Base64.getDecoder().decode(privB64);
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
PublicKey pub = kf.generatePublic(new X509EncodedKeySpec(pubDer));
|
||||
PrivateKey priv = kf.generatePrivate(new PKCS8EncodedKeySpec(privDer));
|
||||
cached = new KeyPair(pub, priv);
|
||||
cachedPublicB64 = pubB64;
|
||||
return cached;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] persisted signing key is corrupt, generating a new pair", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
KeyPair generated = WsSessionCrypto.generateSigningKeyPair();
|
||||
byte[] pubDer = WsSessionCrypto.encodePublicKeySpki(generated.getPublic());
|
||||
byte[] privDer = WsSessionCrypto.encodePrivateKeyPkcs8(generated.getPrivate());
|
||||
String newPubB64 = Base64.getEncoder().withoutPadding().encodeToString(pubDer);
|
||||
String newPrivB64 = Base64.getEncoder().withoutPadding().encodeToString(privDer);
|
||||
|
||||
persist(KEY_PUBLIC, newPubB64);
|
||||
persist(KEY_PRIVATE, newPrivB64);
|
||||
Emulator.getConfig().update(KEY_PUBLIC, newPubB64);
|
||||
Emulator.getConfig().update(KEY_PRIVATE, newPrivB64);
|
||||
|
||||
cached = generated;
|
||||
cachedPublicB64 = newPubB64;
|
||||
LOGGER.info("[ws-crypto] generated a new ECDSA P-256 signing keypair (persisted to emulator_settings)");
|
||||
return cached;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot generate signing keypair", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String publicKeyBase64() {
|
||||
if (cachedPublicB64 == null) get();
|
||||
return cachedPublicB64;
|
||||
}
|
||||
|
||||
private static void persist(String key, String value) {
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"INSERT INTO emulator_settings (`key`, `value`) VALUES (?, ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
|
||||
stmt.setString(1, key);
|
||||
stmt.setString(2, value);
|
||||
stmt.executeUpdate();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] failed to persist " + key + " to emulator_settings (key stays in-memory only)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -1,5 +1,6 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -15,9 +16,8 @@ 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 static final boolean SIGN_ENABLED = Emulator.getConfig().getBoolean("crypto.ws.signing.enabled", false);
|
||||
private KeyPair serverKeyPair;
|
||||
private boolean helloSent = false;
|
||||
private boolean handshakeComplete = false;
|
||||
@@ -35,12 +35,23 @@ public class WsHandshakeHandler extends ChannelInboundHandlerAdapter {
|
||||
try {
|
||||
this.serverKeyPair = WsSessionCrypto.generateEphemeralKeyPair();
|
||||
byte[] spki = WsSessionCrypto.encodePublicKeySpki(serverKeyPair.getPublic());
|
||||
byte[] sigIeee = null;
|
||||
if (SIGN_ENABLED) {
|
||||
KeyPair signingKp = CryptoSigningKeyManager.get();
|
||||
byte[] sigDer = WsSessionCrypto.signEcdsaSha256(signingKp.getPrivate(), spki);
|
||||
sigIeee = WsSessionCrypto.derToIeee1363(sigDer);
|
||||
}
|
||||
|
||||
ByteBuf buf = ctx.alloc().buffer(4 + 1 + 2 + spki.length);
|
||||
int frameLen = 4 + 1 + 2 + spki.length + (sigIeee != null ? 2 + sigIeee.length : 0);
|
||||
ByteBuf buf = ctx.alloc().buffer(frameLen);
|
||||
buf.writeInt(WsSessionCrypto.HANDSHAKE_MAGIC);
|
||||
buf.writeByte(WsSessionCrypto.TYPE_SERVER_HELLO);
|
||||
buf.writeShort(spki.length);
|
||||
buf.writeBytes(spki);
|
||||
if (sigIeee != null) {
|
||||
buf.writeShort(sigIeee.length);
|
||||
buf.writeBytes(sigIeee);
|
||||
}
|
||||
|
||||
ctx.writeAndFlush(buf);
|
||||
helloSent = true;
|
||||
@@ -98,9 +109,7 @@ public class WsHandshakeHandler extends ChannelInboundHandlerAdapter {
|
||||
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());
|
||||
|
||||
@@ -8,6 +8,7 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -96,4 +97,67 @@ public final class WsSessionCrypto {
|
||||
RNG.nextBytes(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
public static KeyPair generateSigningKeyPair() throws GeneralSecurityException {
|
||||
return generateEphemeralKeyPair();
|
||||
}
|
||||
|
||||
public static PrivateKey decodePrivateKeyPkcs8(byte[] pkcs8) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8));
|
||||
}
|
||||
|
||||
public static byte[] encodePrivateKeyPkcs8(PrivateKey privateKey) {
|
||||
return privateKey.getEncoded();
|
||||
}
|
||||
|
||||
public static byte[] signEcdsaSha256(PrivateKey signingKey, byte[] message) throws GeneralSecurityException {
|
||||
Signature sig = Signature.getInstance("SHA256withECDSA");
|
||||
sig.initSign(signingKey);
|
||||
sig.update(message);
|
||||
return sig.sign();
|
||||
}
|
||||
|
||||
public static byte[] derToIeee1363(byte[] der) throws GeneralSecurityException {
|
||||
if (der == null || der.length < 8 || der[0] != 0x30) {
|
||||
throw new GeneralSecurityException("Malformed DER signature");
|
||||
}
|
||||
|
||||
int seqLen;
|
||||
int idx;
|
||||
if ((der[1] & 0x80) == 0) {
|
||||
seqLen = der[1] & 0xff;
|
||||
idx = 2;
|
||||
} else {
|
||||
int lenBytes = der[1] & 0x7f;
|
||||
if (lenBytes > 2) throw new GeneralSecurityException("DER length too big");
|
||||
seqLen = 0;
|
||||
for (int i = 0; i < lenBytes; i++) seqLen = (seqLen << 8) | (der[2 + i] & 0xff);
|
||||
idx = 2 + lenBytes;
|
||||
}
|
||||
if (idx + seqLen > der.length) throw new GeneralSecurityException("DER truncated");
|
||||
|
||||
if (der[idx] != 0x02) throw new GeneralSecurityException("Expected INTEGER r");
|
||||
int rLen = der[idx + 1] & 0xff;
|
||||
int rStart = idx + 2;
|
||||
|
||||
int sHeader = rStart + rLen;
|
||||
if (der[sHeader] != 0x02) throw new GeneralSecurityException("Expected INTEGER s");
|
||||
int sLen = der[sHeader + 1] & 0xff;
|
||||
int sStart = sHeader + 2;
|
||||
|
||||
byte[] r = stripLeadingZero(Arrays.copyOfRange(der, rStart, rStart + rLen));
|
||||
byte[] s = stripLeadingZero(Arrays.copyOfRange(der, sStart, sStart + sLen));
|
||||
|
||||
byte[] out = new byte[64];
|
||||
System.arraycopy(r, 0, out, 32 - r.length, r.length);
|
||||
System.arraycopy(s, 0, out, 64 - s.length, s.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static byte[] stripLeadingZero(byte[] v) {
|
||||
int i = 0;
|
||||
while (i < v.length - 1 && v[i] == 0x00) i++;
|
||||
return Arrays.copyOfRange(v, i, v.length);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user