diff --git a/CHANGELOG.md b/CHANGELOG.md index 44fbd37..1eb1879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.0.9 — FastLoginPlus native login channel association + +- Associate the ProtocolLib `TemporaryPlayer` generated during PacketEvents pre-login callbacks with its actual Netty injector instead of leaving `TemporaryPlayerFactory.getInjectorFromPlayer()` null. FastLoginPlus uses that accessor during Mojang session verification. +- Resolve the real `net.minecraft.network.Connection` from the channel's existing `packet_handler`, and update cached Netty injectors that were created before the native handler was installed. FastLoginPlus needs the genuine Connection (not the bare Netty Channel) to enable packet encryption. +- Explicitly refuse binding a non-active channel or an injector lacking a native Connection; never fall back to cracked login in P2P code and never circumvent Mojang verification. +- Handle FastLoginPlus's legacy method lookup `a(SecretKey)` on remapped Minecraft 1.21.11 where the native Connection method is named `setEncryptionKey(SecretKey)`; fallback only for one unambiguous void/SecretKey method. +- Keep v1.0.8's encryption wrapper constructor fix and v1.0.7's temporary-user receive/send transport. + ## 1.0.8 — Constructor-safe PacketEvents wrapper allocation - Fix `NullPointerException: wrappper.bufferLock is null` when FastLoginPlus sends `LOGIN.SERVER.ENCRYPTION_REQUEST` with a `PacketContainer` constructed from scratch. diff --git a/pom.xml b/pom.xml index 4de86ca..024f859 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.comphenix.protocol ProtocolLib2PacketEvents - 1.0.8 + 1.0.9 jar ProtocolLib2PacketEvents diff --git a/src/main/java/com/comphenix/protocol/ProtocolLib.java b/src/main/java/com/comphenix/protocol/ProtocolLib.java index 1e20b0a..c3a1e37 100644 --- a/src/main/java/com/comphenix/protocol/ProtocolLib.java +++ b/src/main/java/com/comphenix/protocol/ProtocolLib.java @@ -182,6 +182,14 @@ public void onPlayerQuit(PlayerQuitEvent event) { } } + /** Resolve the pre-login facade to its native connection for auth plugins. */ + public com.comphenix.protocol.injector.netty.Injector getTemporaryInjector( + com.github.retrooper.packetevents.protocol.player.User user, + org.bukkit.entity.Player temporary) { + return networkManagerInjector == null ? null + : networkManagerInjector.getTemporaryInjector(user, temporary); + } + @Override public void onDisable() { if (internalListener != null) { diff --git a/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java b/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java index 68eca5a..449767c 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java @@ -55,7 +55,10 @@ public Injector fromPlayer(Player player) { public Injector fromChannel(Channel channel) { if (channel == null) return EmptyInjector.WITHOUT_PLAYER; Injector existing = byChannel.get(channel); - if (existing != null) return existing; + if (existing != null) { + if (existing instanceof NettyChannelInjector netty) netty.refreshNativeConnection(); + return existing; + } Injector created = new NettyChannelInjector(null, null, channel, listeners, this, reporter); Injector previous = byChannel.putIfAbsent(channel, created); return previous == null ? created : previous; diff --git a/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java index 7da14cd..7fba2cd 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java @@ -32,7 +32,11 @@ public class NettyChannelInjector implements Injector { private static final String ENCODER = "p2p-wire-encoder"; private static final String OUTBOUND = "p2p-outbound"; private volatile Player player; - private final Object networkManager; + // The Netty channel is discovered before the native Minecraft Connection + // handler may be in the pipeline. Refresh it as soon as the first LOGIN + // packet arrives so FastLoginPlus can enable AES encryption on the real + // NMS Connection. It must not be the raw Netty Channel or a dummy handle. + private volatile Object networkManager; private final Channel channel; private final ListenerManager listenerManager; private final InjectionFactory factory; @@ -49,11 +53,34 @@ public NettyChannelInjector(Player player, Object networkManager, Channel channe this.player = player; this.networkManager = networkManager; this.channel = channel; + refreshNativeConnection(); this.listenerManager = listenerManager; this.factory = factory; this.reporter = reporter; this.protocol = player == null ? PacketType.Protocol.HANDSHAKING : PacketType.Protocol.PLAY; } + /** + * Find the actual net.minecraft.network.Connection handler already installed + * by the Minecraft server. No field/type guessing, no reflection into the + * packet pipeline and no mutation of its handler order. + */ + public void refreshNativeConnection() { + if (networkManager != null || channel == null) return; + io.netty.channel.ChannelHandler packetHandler = channel.pipeline().get("packet_handler"); + if (packetHandler == null) return; + for (Class clazz = packetHandler.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + if ("net.minecraft.network.Connection".equals(clazz.getName())) { + networkManager = packetHandler; + return; + } + } + } + + public boolean hasNativeConnection() { + refreshNativeConnection(); + return networkManager != null; + } + public SocketAddress getAddress() { return channel == null ? null : channel.remoteAddress(); } public int getProtocolVersion() { if (player == null || PacketEvents.getAPI() == null) return -1; diff --git a/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java index 0100b05..d096e97 100644 --- a/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java +++ b/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java @@ -41,6 +41,29 @@ public NetworkManagerInjector(Plugin plugin, ListenerManager listenerManager, Er public Injector getInjector(Player player) { return factory.fromPlayer(player); } + /** + * ProtocolLib's temporary player is not a CraftPlayer, so never pass it to + * PacketEvents PlayerManager.getChannel(Player): resolve the channel from + * the pre-login User instead. FastLoginPlus relies on this exact facade's + * getInjectorFromPlayer() to locate the real Netty channel and NMS + * Connection for session verification and encryption. + */ + public Injector getTemporaryInjector(com.github.retrooper.packetevents.protocol.player.User user, + Player temporary) { + if (closed || user == null || temporary == null) return null; + if (!(user.getChannel() instanceof Channel channel) || !channel.isActive()) return null; + Injector injector = factory.fromChannel(channel); + if (!(injector instanceof com.comphenix.protocol.injector.netty.channel.NettyChannelInjector netty)) { + return null; + } + netty.refreshNativeConnection(); + // A stale/partial channel cannot authenticate a login. Fail closed: + // don't give FastLoginPlus an injector whose networkManager is null. + if (!netty.hasNativeConnection()) return null; + netty.setPlayer(temporary); + return netty; + } + public void invalidate(Player player) { if (player == null) return; Injector injector = factory.invalidate(player); diff --git a/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java b/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java index e395a1c..85c6ac1 100644 --- a/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java +++ b/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java @@ -27,6 +27,15 @@ public static Player create(User user) { } Player player = TemporaryPlayerFactory.createTemporaryPlayer(); USERS.put(player, user); + // The pre-login facade must carry the same native Connection as its + // PacketEvents User. Without it, FastLoginPlus's ProtocolLibCompat + // considers every player disconnected during Mojang session checks. + org.bukkit.plugin.Plugin plugin = com.comphenix.protocol.ProtocolLibrary.getPlugin(); + if (plugin instanceof com.comphenix.protocol.ProtocolLib bridge) { + com.comphenix.protocol.injector.netty.Injector injector = + bridge.getTemporaryInjector(user, player); + if (injector != null) TemporaryPlayerFactory.setInjectorForPlayer(player, injector); + } return player; } diff --git a/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java b/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java index d463262..4af4fb7 100644 --- a/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java +++ b/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java @@ -102,6 +102,24 @@ public Method getMethodByParameters(String name, Class... parameters) { return accessible(method); } } + // FastLoginPlus still asks for the obfuscated Minecraft Connection + // method "a(SecretKey)". On remapped Paper/Leaf 1.21.11 the equivalent + // is named setEncryptionKey(SecretKey). Resolve by exact signature only + // when it is unique: never guess an arbitrary encryption/packet method. + if ("a".equals(name) && parameters.length == 1 + && parameters[0] == javax.crypto.SecretKey.class) { + Method found = null; + for (Method method : getMethods()) { + if (!java.util.Arrays.equals(method.getParameterTypes(), parameters) + || method.getReturnType() != void.class) continue; + if (found != null && !found.equals(method)) { + throw new IllegalArgumentException("Ambiguous SecretKey encryption methods on " + + source.getName()); + } + found = method; + } + if (found != null) return accessible(found); + } throw new IllegalArgumentException("Unable to find method " + name + " on " + source.getName()); } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index a6cbd99..b9def08 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -10,7 +10,7 @@ main: com.comphenix.protocol.ProtocolLib version: 5.4.0 author: CyoriaSMP Team description: A drop-in ProtocolLib compatibility layer powered by PacketEvents. -p2p-version: 1.0.8 +p2p-version: 1.0.9 protocol-lib-api-version: 5.4.0 depend: [packetevents] provides: [ProtocolLib] diff --git a/src/test/java/com/comphenix/protocol/injector/netty/channel/PreLoginNativeConnectionTest.java b/src/test/java/com/comphenix/protocol/injector/netty/channel/PreLoginNativeConnectionTest.java new file mode 100644 index 0000000..ceed6e3 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/injector/netty/channel/PreLoginNativeConnectionTest.java @@ -0,0 +1,32 @@ +package com.comphenix.protocol.injector.netty.channel; + +import io.netty.channel.embedded.EmbeddedChannel; +import java.lang.reflect.Field; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class PreLoginNativeConnectionTest { + @Test + void findsLateNativePacketHandlerForFastLoginCipher() throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(); + NettyChannelInjector injector = new NettyChannelInjector(null, null, channel, null, null, null); + assertFalse(injector.hasNativeConnection(), "cannot fabricate a native NMS Connection"); + net.minecraft.network.Connection nativeConn = new net.minecraft.network.Connection(); + channel.pipeline().addLast("packet_handler", nativeConn); + injector.refreshNativeConnection(); + assertTrue(injector.hasNativeConnection()); + Field field = NettyChannelInjector.class.getDeclaredField("networkManager"); + field.setAccessible(true); + assertSame(nativeConn, field.get(injector), "FastLoginPlus must receive actual NMS packet handler"); + channel.finishAndReleaseAll(); + } + + @Test + void neverConfusesOtherChannelHandlerWithNmsConnection() { + EmbeddedChannel channel = new EmbeddedChannel(); + NettyChannelInjector injector = new NettyChannelInjector(null, null, channel, null, null, null); + channel.pipeline().addLast("packet_handler", new io.netty.channel.ChannelInboundHandlerAdapter()); + assertFalse(injector.hasNativeConnection()); + channel.finishAndReleaseAll(); + } +} diff --git a/src/test/java/com/comphenix/protocol/reflect/FuzzyReflectionSecretKeyTest.java b/src/test/java/com/comphenix/protocol/reflect/FuzzyReflectionSecretKeyTest.java new file mode 100644 index 0000000..62a04d3 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/reflect/FuzzyReflectionSecretKeyTest.java @@ -0,0 +1,40 @@ +package com.comphenix.protocol.reflect; + +import javax.crypto.SecretKey; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class FuzzyReflectionSecretKeyTest { + public static class ModernConnection { + public void setEncryptionKey(SecretKey key) { } + } + public static class AmbiguousConnection { + public void setEncryptionKey(SecretKey key) { } + public void anotherMethod(SecretKey key) { } + } + public static class OtherMethod { + public void send(Object obj) { } + } + + @Test + void remappedModernMinecraftKeySetterIsDiscoverableByOldName() { + var result = FuzzyReflection.fromClass(ModernConnection.class) + .getMethodByParameters("a", SecretKey.class); + assertEquals("setEncryptionKey", result.getName()); + } + + @Test + void ambiguousCryptoMethodsDoNotGetPickedArbitrarily() { + assertThrows(IllegalArgumentException.class, + () -> FuzzyReflection.fromClass(AmbiguousConnection.class) + .getMethodByParameters("a", SecretKey.class)); + } + + @Test + void normalMethodNameLookupStaysStrict() { + assertThrows(IllegalArgumentException.class, + () -> FuzzyReflection.fromClass(OtherMethod.class) + .getMethodByParameters("a", Object.class)); + } +} diff --git a/src/test/java/net/minecraft/network/Connection.java b/src/test/java/net/minecraft/network/Connection.java new file mode 100644 index 0000000..486d0c8 --- /dev/null +++ b/src/test/java/net/minecraft/network/Connection.java @@ -0,0 +1,4 @@ +package net.minecraft.network; + +/** Test-only stand-in for Paper's registered packet_handler. */ +public class Connection extends io.netty.channel.ChannelInboundHandlerAdapter { }