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.
|
||||
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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+5
-1
@@ -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."));
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
@@ -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> world, BlockPos 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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.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()));
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user