Add: SignatureService

Improve requested room validation by using newly added SignatureService
This commit is contained in:
iceBear67 committed 2025-01-06 17:53:47 +08:00
1 parent d1747f5e0c
commit aadec09c45
9 files changed
+130 -68

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ dependencies {
// Fabric API. This is technically optional, but you probably want it anyway. // Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
shadow('com.maxmind.geoip2:geoip2:4.2.0') shadow('com.maxmind.geoip2:geoip2:4.2.0')
implementation("io.javalin:javalin:6.4.0")
shadow('com.google.inject:guice:7.0.0') { shadow('com.google.inject:guice:7.0.0') {
exclude group: 'com.google.guava', module: 'guava' exclude group: 'com.google.guava', module: 'guava'
exclude group: 'com.google.guava', module: 'listenablefuture' exclude group: 'com.google.guava', module: 'listenablefuture'
@@ -43,6 +43,7 @@ public class SFCraftInitializer extends GuiceModInitializer {
binder().bind(GeoIPService.class).to(MaxMindGeoIPService.class); binder().bind(GeoIPService.class).to(MaxMindGeoIPService.class);
binder().bind(RandomEventRegistry.class).to(SFRandomEventRegistry.class); binder().bind(RandomEventRegistry.class).to(SFRandomEventRegistry.class);
binder().bind(SimpleMessageDecorator.class).in(Singleton.class); binder().bind(SimpleMessageDecorator.class).in(Singleton.class);
binder().bind(SignatureService.class).in(Singleton.class);
binder().bind(RoomTeleporter.class).in(Singleton.class); binder().bind(RoomTeleporter.class).in(Singleton.class);
binder().bind(RoomRegistry.class).to(SimpleRoomRegistry.class).in(Singleton.class); binder().bind(RoomRegistry.class).to(SimpleRoomRegistry.class).in(Singleton.class);
} }
@@ -11,4 +11,6 @@ public class SFConfig {
public boolean enableOfflineExempt = true; public boolean enableOfflineExempt = true;
public String domain = "localhost"; public String domain = "localhost";
public String serverSecret = RandomStringUtils.random(32); public String serverSecret = RandomStringUtils.random(32);
public int httpPort = 8080;
public long maxSchematicSize = 100000;
} }
@@ -3,9 +3,11 @@ package io.ib67.sfcraft.mixin.server.subserver;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import io.ib67.sfcraft.SFCraft; import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.module.RoomModule; import io.ib67.sfcraft.module.RoomModule;
import io.ib67.sfcraft.module.SignatureService;
import io.ib67.sfcraft.registry.RoomRegistry; import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.room.CookieState; import io.ib67.sfcraft.room.CookieState;
import io.ib67.sfcraft.room.RequestedRoom; import io.ib67.sfcraft.room.RequestedRoom;
import io.netty.buffer.Unpooled;
import net.minecraft.network.ClientConnection; import net.minecraft.network.ClientConnection;
import net.minecraft.network.packet.c2s.common.CookieResponseC2SPacket; import net.minecraft.network.packet.c2s.common.CookieResponseC2SPacket;
import net.minecraft.network.packet.s2c.common.CookieRequestS2CPacket; import net.minecraft.network.packet.s2c.common.CookieRequestS2CPacket;
@@ -68,6 +70,7 @@ public abstract class ServerLoginNetworkHandlerMixin {
if (packet.key().equals(ROOM_COOKIE)) { if (packet.key().equals(ROOM_COOKIE)) {
if (sf$cookieState != SENT) throw new IllegalStateException("Protocol error"); if (sf$cookieState != SENT) throw new IllegalStateException("Protocol error");
var roomSvc = SFCraft.getInjector().getInstance(RoomModule.class); var roomSvc = SFCraft.getInjector().getInstance(RoomModule.class);
var signSvc = SFCraft.getInjector().getInstance(SignatureService.class);
try { try {
if (packet.payload().length == 0) { if (packet.payload().length == 0) {
// clean reconnect. // clean reconnect.
@@ -76,7 +79,8 @@ public abstract class ServerLoginNetworkHandlerMixin {
ci.cancel(); ci.cancel();
return; 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) { } catch (Exception t) {
LOGGER.error("Failed to read cookie: {0}", t); LOGGER.error("Failed to read cookie: {0}", t);
this.disconnect(Text.of("Protocol error.")); this.disconnect(Text.of("Protocol error."));
@@ -52,6 +52,7 @@ import static io.ib67.sfcraft.util.SFConsts.COMMAND_RECO;
@Log4j2 @Log4j2
public class RoomModule extends ServerModule { public class RoomModule extends ServerModule {
public static final String SIGN_TOPIC = "room";
private static final byte[] EMPTY = new byte[0]; private static final byte[] EMPTY = new byte[0];
@Inject @Inject
private RoomRegistry roomRegistry; private RoomRegistry roomRegistry;
@@ -59,14 +60,11 @@ public class RoomModule extends ServerModule {
private MinecraftServerSupplier serverSupplier; private MinecraftServerSupplier serverSupplier;
@Inject @Inject
private SFConfig config; private SFConfig config;
private Key key;
private final List<Pair<RegistryKey<World>, BlockPos>> pregenQueue = new ArrayList<>(); private final List<Pair<RegistryKey<World>, BlockPos>> pregenQueue = new ArrayList<>();
private final Map<UUID, GameProfile> uuidMapper = new ConcurrentHashMap<>(); private final Map<UUID, GameProfile> uuidMapper = new ConcurrentHashMap<>();
private final Map<Identifier, GameRules> gameRulesPerRoom = new HashMap<>();
@Override @Override
public void onInitialize() { public void onInitialize() {
key = getKeyFromPassword(config.serverSecret, RandomStringUtils.random(16));
CommandRegistrationCallback.EVENT.register(this::registerCommand); CommandRegistrationCallback.EVENT.register(this::registerCommand);
} }
@@ -103,63 +101,6 @@ public class RoomModule extends ServerModule {
return 1; 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> world, BlockPos spawnPos) { public void enqueuePregen(RegistryKey<World> world, BlockPos spawnPos) {
pregenQueue.add(new Pair<>(world, spawnPos)); pregenQueue.add(new Pair<>(world, spawnPos));
} }
@@ -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<ByteBuf, Signature> 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();
}
}
}
@@ -111,7 +111,12 @@ public class CreativeRoomModule extends ServerModule {
private int gotoPlayground(CommandContext<ServerCommandSource> serverCommandSourceCommandContext) { private int gotoPlayground(CommandContext<ServerCommandSource> serverCommandSourceCommandContext) {
var player = serverCommandSourceCommandContext.getSource().getPlayer(); 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; return 0;
} }
@@ -1,6 +1,12 @@
package io.ib67.sfcraft.room; 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.Identifier;
import net.minecraft.util.Uuids;
import net.minecraft.util.dynamic.Codecs;
import java.util.UUID; import java.util.UUID;
@@ -9,4 +15,10 @@ public record RequestedRoom(
String profileName, String profileName,
UUID profileUuid UUID profileUuid
) { ) {
public static final PacketCodec<ByteBuf, RequestedRoom> PACKET_CODEC = PacketCodec.tuple(
Identifier.PACKET_CODEC, RequestedRoom::identifier,
PacketCodecs.STRING, RequestedRoom::profileName,
Uuids.PACKET_CODEC, RequestedRoom::profileUuid,
RequestedRoom::new
);
} }
@@ -5,9 +5,10 @@ import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.config.SFConfig; import io.ib67.sfcraft.config.SFConfig;
import io.ib67.sfcraft.inject.MinecraftServerSupplier; import io.ib67.sfcraft.inject.MinecraftServerSupplier;
import io.ib67.sfcraft.module.RoomModule; import io.ib67.sfcraft.module.RoomModule;
import io.ib67.sfcraft.module.SignatureService;
import io.ib67.sfcraft.registry.RoomRegistry; import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.subserver.Room; 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.PacketCallbacks;
import net.minecraft.network.packet.s2c.common.ServerTransferS2CPacket; import net.minecraft.network.packet.s2c.common.ServerTransferS2CPacket;
import net.minecraft.network.packet.s2c.common.StoreCookieS2CPacket; import net.minecraft.network.packet.s2c.common.StoreCookieS2CPacket;
@@ -22,6 +23,8 @@ public class RoomTeleporter {
@Inject @Inject
private RoomModule roomModule; private RoomModule roomModule;
@Inject @Inject
private SignatureService signatureModule;
@Inject
private MinecraftServerSupplier serverSupplier; private MinecraftServerSupplier serverSupplier;
@Inject @Inject
private SFConfig config; private SFConfig config;
@@ -35,16 +38,25 @@ public class RoomTeleporter {
} }
var finalUuid = roomModule.generateIdForRoom(player.getGameProfile(), player.getName().getLiteralString(), room.getServerIdentifier()); var finalUuid = roomModule.generateIdForRoom(player.getGameProfile(), player.getName().getLiteralString(), room.getServerIdentifier());
var sess = room.getPlayerManager().createSessionFor(finalUuid); var sess = room.getPlayerManager().createSessionFor(finalUuid);
var name = player.getName().getLiteralString();
if (player.getServer().getWorld(sess.getSpawnPosition().dimension()) == null) { if (player.getServer().getWorld(sess.getSpawnPosition().dimension()) == null) {
throw new IllegalStateException("world isn't exist"); throw new IllegalStateException("world isn't exist");
} }
var networkHandler = player.networkHandler; var networkHandler = player.networkHandler;
networkHandler.reconfigure(); networkHandler.reconfigure();
networkHandler.send(new StoreCookieS2CPacket(ROOM_COOKIE, roomModule.serializeRequestedRoom(new RequestedRoom( var request = new RequestedRoom(room.getServerIdentifier(), name, finalUuid);
room.getServerIdentifier(), var requestBuf = Unpooled.buffer();
player.getName().getLiteralString(), RequestedRoom.PACKET_CODEC.encode(requestBuf, request);
finalUuid var cookie = signatureModule.createSignature(
))), PacketCallbacks.always(() -> { 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())); networkHandler.sendPacket(new ServerTransferS2CPacket(config.domain, serverSupplier.get().getServerPort()));
})); }));
} }