Add: SignatureService
Improve requested room validation by using newly added SignatureService
This commit is contained in:
1 parent
d1747f5e0c
commit
aadec09c45
9 files changed
+130
-68
No files matched your search
@@ -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;
|
||||||
}
|
}
|
||||||
+5
-1
@@ -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()));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user