Skip to content

Commit

Permalink
Add: SignatureService
Browse files Browse the repository at this point in the history
Improve requested room validation by using newly added SignatureService
  • Loading branch information
iceBear67 committed Jan 6, 2025
1 parent d1747f5 commit aadec09
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 68 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/ib67/sfcraft/SFCraftInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ private void registerServices() {
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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/ib67/sfcraft/config/SFConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
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;
Expand Down Expand Up @@ -68,6 +70,7 @@ void restoreVerify(CallbackInfo ci) {
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.
Expand All @@ -76,7 +79,8 @@ void restoreVerify(CallbackInfo ci) {
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."));
Expand Down
61 changes: 1 addition & 60 deletions src/main/java/io/ib67/sfcraft/module/RoomModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,19 @@

@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;
@Inject
private MinecraftServerSupplier serverSupplier;
@Inject
private SFConfig config;
private Key key;
private final List<Pair<RegistryKey<World>, BlockPos>> pregenQueue = new ArrayList<>();
private final Map<UUID, GameProfile> uuidMapper = new ConcurrentHashMap<>();
private final Map<Identifier, GameRules> gameRulesPerRoom = new HashMap<>();

@Override
public void onInitialize() {
key = getKeyFromPassword(config.serverSecret, RandomStringUtils.random(16));
CommandRegistrationCallback.EVENT.register(this::registerCommand);
}

Expand Down Expand Up @@ -103,63 +101,6 @@ private int onCleanReconnect(CommandContext<ServerCommandSource> serverCommandSo
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) {
pregenQueue.add(new Pair<>(world, spawnPos));
}
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/io/ib67/sfcraft/module/SignatureService.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ private void registerCommand(CommandDispatcher<ServerCommandSource> dispatcher,

private int gotoPlayground(CommandContext<ServerCommandSource> 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;
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/ib67/sfcraft/room/RequestedRoom.java
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -9,4 +15,10 @@ public record RequestedRoom(
String profileName,
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
);
}
24 changes: 18 additions & 6 deletions src/main/java/io/ib67/sfcraft/room/RoomTeleporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
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;
Expand All @@ -22,6 +23,8 @@ public class RoomTeleporter {
@Inject
private RoomModule roomModule;
@Inject
private SignatureService signatureModule;
@Inject
private MinecraftServerSupplier serverSupplier;
@Inject
private SFConfig config;
Expand All @@ -35,16 +38,25 @@ public void teleportTo(Room room, ServerPlayerEntity player) {
}
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()));
}));
}
Expand Down

0 comments on commit aadec09

Please sign in to comment.