diff --git a/build.gradle b/build.gradle index cece967..ee60f96 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" shadow('com.maxmind.geoip2:geoip2:4.2.0') + implementation("io.javalin:javalin:6.4.0") shadow('com.google.inject:guice:7.0.0') { exclude group: 'com.google.guava', module: 'guava' exclude group: 'com.google.guava', module: 'listenablefuture' diff --git a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java index 0ea49c4..bed0e3f 100644 --- a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java +++ b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java @@ -43,6 +43,7 @@ public class SFCraftInitializer extends GuiceModInitializer { binder().bind(GeoIPService.class).to(MaxMindGeoIPService.class); binder().bind(RandomEventRegistry.class).to(SFRandomEventRegistry.class); binder().bind(SimpleMessageDecorator.class).in(Singleton.class); + binder().bind(SignatureService.class).in(Singleton.class); binder().bind(RoomTeleporter.class).in(Singleton.class); binder().bind(RoomRegistry.class).to(SimpleRoomRegistry.class).in(Singleton.class); } diff --git a/src/main/java/io/ib67/sfcraft/config/SFConfig.java b/src/main/java/io/ib67/sfcraft/config/SFConfig.java index 0bbc926..8b73dd2 100644 --- a/src/main/java/io/ib67/sfcraft/config/SFConfig.java +++ b/src/main/java/io/ib67/sfcraft/config/SFConfig.java @@ -11,4 +11,6 @@ public class SFConfig { public boolean enableOfflineExempt = true; public String domain = "localhost"; public String serverSecret = RandomStringUtils.random(32); + public int httpPort = 8080; + public long maxSchematicSize = 100000; } diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/subserver/ServerLoginNetworkHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/subserver/ServerLoginNetworkHandlerMixin.java index 64a8715..edd4c77 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/subserver/ServerLoginNetworkHandlerMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/subserver/ServerLoginNetworkHandlerMixin.java @@ -3,9 +3,11 @@ package io.ib67.sfcraft.mixin.server.subserver; import com.mojang.authlib.GameProfile; import io.ib67.sfcraft.SFCraft; import io.ib67.sfcraft.module.RoomModule; +import io.ib67.sfcraft.module.SignatureService; import io.ib67.sfcraft.registry.RoomRegistry; import io.ib67.sfcraft.room.CookieState; import io.ib67.sfcraft.room.RequestedRoom; +import io.netty.buffer.Unpooled; import net.minecraft.network.ClientConnection; import net.minecraft.network.packet.c2s.common.CookieResponseC2SPacket; import net.minecraft.network.packet.s2c.common.CookieRequestS2CPacket; @@ -68,6 +70,7 @@ public abstract class ServerLoginNetworkHandlerMixin { if (packet.key().equals(ROOM_COOKIE)) { if (sf$cookieState != SENT) throw new IllegalStateException("Protocol error"); var roomSvc = SFCraft.getInjector().getInstance(RoomModule.class); + var signSvc = SFCraft.getInjector().getInstance(SignatureService.class); try { if (packet.payload().length == 0) { // clean reconnect. @@ -76,7 +79,8 @@ public abstract class ServerLoginNetworkHandlerMixin { ci.cancel(); return; } - this.sf$room = roomSvc.readRoomRequest(packet.payload()); + var sign = signSvc.readSignature(Unpooled.wrappedBuffer(packet.payload())); + this.sf$room = RequestedRoom.PACKET_CODEC.decode(Unpooled.wrappedBuffer(sign.data())); } catch (Exception t) { LOGGER.error("Failed to read cookie: {0}", t); this.disconnect(Text.of("Protocol error.")); diff --git a/src/main/java/io/ib67/sfcraft/module/RoomModule.java b/src/main/java/io/ib67/sfcraft/module/RoomModule.java index 92be63a..575c4b2 100644 --- a/src/main/java/io/ib67/sfcraft/module/RoomModule.java +++ b/src/main/java/io/ib67/sfcraft/module/RoomModule.java @@ -52,6 +52,7 @@ import static io.ib67.sfcraft.util.SFConsts.COMMAND_RECO; @Log4j2 public class RoomModule extends ServerModule { + public static final String SIGN_TOPIC = "room"; private static final byte[] EMPTY = new byte[0]; @Inject private RoomRegistry roomRegistry; @@ -59,14 +60,11 @@ public class RoomModule extends ServerModule { private MinecraftServerSupplier serverSupplier; @Inject private SFConfig config; - private Key key; private final List, BlockPos>> pregenQueue = new ArrayList<>(); private final Map uuidMapper = new ConcurrentHashMap<>(); - private final Map gameRulesPerRoom = new HashMap<>(); @Override public void onInitialize() { - key = getKeyFromPassword(config.serverSecret, RandomStringUtils.random(16)); CommandRegistrationCallback.EVENT.register(this::registerCommand); } @@ -103,63 +101,6 @@ public class RoomModule extends ServerModule { return 1; } - @SneakyThrows - byte[] encrypt(byte[] b) { - var cipher = Cipher.getInstance("AES"); - cipher.init(Cipher.ENCRYPT_MODE, key); - return cipher.doFinal(b); - } - - @SneakyThrows - byte[] decrypt(byte[] b) { - var cipher = Cipher.getInstance("AES"); - cipher.init(Cipher.DECRYPT_MODE, key); - return cipher.doFinal(b); - } - - public byte[] serializeRequestedRoom(RequestedRoom room) { - var buf = Unpooled.buffer(); - ByteBuf nbuf = null; - try { - Identifier.PACKET_CODEC.encode(buf, room.identifier()); - PacketCodecs.GAME_PROFILE.encode(buf, new GameProfile(room.profileUuid(), room.profileName())); - var encrypted = encrypt(buf.array()); - nbuf = Unpooled.buffer(); - nbuf.writeShortLE(encrypted.length); - nbuf.writeBytes(encrypted); - return nbuf.array(); - } finally { - if (nbuf != null) nbuf.release(); - buf.release(); - } - } - - public RequestedRoom readRoomRequest(byte[] payload) { - ByteBuf buf = null; - ByteBuf decrypted = null; - try { - buf = Unpooled.wrappedBuffer(payload); - var len = buf.readUnsignedShortLE(); - var content = new byte[len]; - buf.readBytes(content); - decrypted = Unpooled.wrappedBuffer(decrypt(content)); - - var identifier = Identifier.PACKET_CODEC.decode(decrypted); - var profile = PacketCodecs.GAME_PROFILE.decode(decrypted); - return new RequestedRoom(identifier, profile.getName(), profile.getId()); - } finally { - if (buf != null) buf.release(); //todo - if (decrypted != null) decrypted.release(); - } - } - - @SneakyThrows - private static SecretKey getKeyFromPassword(String password, String salt) { - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256); - return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); - } - public void enqueuePregen(RegistryKey world, BlockPos spawnPos) { pregenQueue.add(new Pair<>(world, spawnPos)); } diff --git a/src/main/java/io/ib67/sfcraft/module/SignatureService.java b/src/main/java/io/ib67/sfcraft/module/SignatureService.java new file mode 100644 index 0000000..83d4f6a --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/SignatureService.java @@ -0,0 +1,84 @@ +package io.ib67.sfcraft.module; + +import com.google.inject.Inject; +import io.ib67.sfcraft.config.SFConfig; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.SneakyThrows; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import org.apache.commons.lang3.RandomStringUtils; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.Key; +import java.security.MessageDigest; +import java.security.spec.KeySpec; +import java.util.Arrays; + +public class SignatureService { + private Key key; + + @SneakyThrows + @Inject + public SignatureService(SFConfig config) { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(config.serverSecret.toCharArray(), RandomStringUtils.random(16).getBytes(), 2048, 256); + key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); + } + + public Signature readSignature(ByteBuf buf) { + var hash = new byte[28]; + buf.readBytes(hash); + var sign = Signature.PACKET_CODEC.decode(buf); + if (!Arrays.equals(sign.calcHash(key), hash)) { + throw new IllegalArgumentException("Invalid signature."); + } + if (sign.validUntil < System.currentTimeMillis()) { + throw new IllegalArgumentException("Expired signature."); + } + return sign; + } + + public byte[] createSignature(Signature signature) { + var buf = Unpooled.buffer(); + writeSignature(buf, signature); + return buf.array(); + } + + @SneakyThrows + public void writeSignature(ByteBuf buf, Signature signature) { + buf.writeBytes(signature.calcHash(key)); + Signature.PACKET_CODEC.encode(buf, signature); + } + + public record Signature( + String topic, + String issuer, + int permission, + long validUntil, + byte[] data + ) { + public static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + PacketCodecs.STRING, Signature::topic, + PacketCodecs.STRING, Signature::issuer, + PacketCodecs.VAR_INT, Signature::permission, + PacketCodecs.VAR_LONG, Signature::validUntil, + PacketCodecs.BYTE_ARRAY, Signature::data, + Signature::new + ); + + @SneakyThrows + private byte[] calcHash(Key key) { + var hmac = MessageDigest.getInstance("SHA3-224"); + hmac.update(topic.getBytes()); + hmac.update(issuer.getBytes()); + hmac.update(ByteBuffer.allocate(4).putInt(permission).array()); + hmac.update(ByteBuffer.allocate(8).putLong(validUntil).array()); + hmac.update(key.getEncoded()); + return hmac.digest(); + } + } +} diff --git a/src/main/java/io/ib67/sfcraft/module/room/CreativeRoomModule.java b/src/main/java/io/ib67/sfcraft/module/room/CreativeRoomModule.java index abb205f..802758d 100644 --- a/src/main/java/io/ib67/sfcraft/module/room/CreativeRoomModule.java +++ b/src/main/java/io/ib67/sfcraft/module/room/CreativeRoomModule.java @@ -111,7 +111,12 @@ public class CreativeRoomModule extends ServerModule { private int gotoPlayground(CommandContext serverCommandSourceCommandContext) { var player = serverCommandSourceCommandContext.getSource().getPlayer(); - teleporter.teleportTo(room, player); + try { + teleporter.teleportTo(room, player); + }catch (Exception e){ + player.networkHandler.disconnect(Text.of(e.getMessage())); + e.printStackTrace(); + } return 0; } diff --git a/src/main/java/io/ib67/sfcraft/room/RequestedRoom.java b/src/main/java/io/ib67/sfcraft/room/RequestedRoom.java index 15429ad..aff7fd9 100644 --- a/src/main/java/io/ib67/sfcraft/room/RequestedRoom.java +++ b/src/main/java/io/ib67/sfcraft/room/RequestedRoom.java @@ -1,6 +1,12 @@ package io.ib67.sfcraft.room; +import com.mojang.serialization.Codec; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; +import net.minecraft.util.dynamic.Codecs; import java.util.UUID; @@ -9,4 +15,10 @@ public record RequestedRoom( String profileName, UUID profileUuid ) { + public static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + Identifier.PACKET_CODEC, RequestedRoom::identifier, + PacketCodecs.STRING, RequestedRoom::profileName, + Uuids.PACKET_CODEC, RequestedRoom::profileUuid, + RequestedRoom::new + ); } diff --git a/src/main/java/io/ib67/sfcraft/room/RoomTeleporter.java b/src/main/java/io/ib67/sfcraft/room/RoomTeleporter.java index 52df847..6ea0342 100644 --- a/src/main/java/io/ib67/sfcraft/room/RoomTeleporter.java +++ b/src/main/java/io/ib67/sfcraft/room/RoomTeleporter.java @@ -5,9 +5,10 @@ import io.ib67.sfcraft.SFCraft; import io.ib67.sfcraft.config.SFConfig; import io.ib67.sfcraft.inject.MinecraftServerSupplier; import io.ib67.sfcraft.module.RoomModule; +import io.ib67.sfcraft.module.SignatureService; import io.ib67.sfcraft.registry.RoomRegistry; import io.ib67.sfcraft.subserver.Room; -import io.ib67.sfcraft.util.Helper; +import io.netty.buffer.Unpooled; import net.minecraft.network.PacketCallbacks; import net.minecraft.network.packet.s2c.common.ServerTransferS2CPacket; import net.minecraft.network.packet.s2c.common.StoreCookieS2CPacket; @@ -22,6 +23,8 @@ public class RoomTeleporter { @Inject private RoomModule roomModule; @Inject + private SignatureService signatureModule; + @Inject private MinecraftServerSupplier serverSupplier; @Inject private SFConfig config; @@ -35,16 +38,25 @@ public class RoomTeleporter { } var finalUuid = roomModule.generateIdForRoom(player.getGameProfile(), player.getName().getLiteralString(), room.getServerIdentifier()); var sess = room.getPlayerManager().createSessionFor(finalUuid); + var name = player.getName().getLiteralString(); if (player.getServer().getWorld(sess.getSpawnPosition().dimension()) == null) { throw new IllegalStateException("world isn't exist"); } var networkHandler = player.networkHandler; networkHandler.reconfigure(); - networkHandler.send(new StoreCookieS2CPacket(ROOM_COOKIE, roomModule.serializeRequestedRoom(new RequestedRoom( - room.getServerIdentifier(), - player.getName().getLiteralString(), - finalUuid - ))), PacketCallbacks.always(() -> { + var request = new RequestedRoom(room.getServerIdentifier(), name, finalUuid); + var requestBuf = Unpooled.buffer(); + RequestedRoom.PACKET_CODEC.encode(requestBuf, request); + var cookie = signatureModule.createSignature( + new SignatureService.Signature( + RoomModule.SIGN_TOPIC, + name, + 0, + System.currentTimeMillis() + 10000, + requestBuf.array() + ) + ); + networkHandler.send(new StoreCookieS2CPacket(ROOM_COOKIE, cookie), PacketCallbacks.always(() -> { networkHandler.sendPacket(new ServerTransferS2CPacket(config.domain, serverSupplier.get().getServerPort())); })); }