From 11a8184c3e14fa89b3baccdf96effd7f176bd241 Mon Sep 17 00:00:00 2001 From: games647 Date: Tue, 26 Jul 2022 19:29:34 +0200 Subject: [PATCH] Add StructureModifier for extracting the signature data in chat and login packets (#1742) --- .../protocol/events/AbstractStructure.java | 34 ++++++ .../protocol/utility/MinecraftReflection.java | 30 +++++- .../protocol/wrappers/BukkitConverters.java | 46 ++++++++ .../comphenix/protocol/wrappers/Either.java | 100 ++++++++++++++++++ .../wrappers/WrappedSaltedSignature.java | 90 ++++++++++++++++ .../protocol/events/PacketContainerTest.java | 42 ++++++++ .../utility/MinecraftReflectionTest.java | 6 ++ .../wrappers/BukkitConvertersTest.java | 17 +++ .../protocol/wrappers/EitherTest.java | 35 ++++++ .../wrappers/WrappedSaltedSignatureTest.java | 87 +++++++++++++++ 10 files changed, 483 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/comphenix/protocol/wrappers/Either.java create mode 100644 src/main/java/com/comphenix/protocol/wrappers/WrappedSaltedSignature.java create mode 100644 src/test/java/com/comphenix/protocol/wrappers/EitherTest.java create mode 100644 src/test/java/com/comphenix/protocol/wrappers/WrappedSaltedSignatureTest.java diff --git a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java index 56fbf694..0c24d89e 100644 --- a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java +++ b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java @@ -929,6 +929,40 @@ public abstract class AbstractStructure { BukkitConverters.getWrappedPublicKeyDataConverter()); } + /** + * @return read/write structure for login encryption packets + */ + public StructureModifier> getLoginSignatures() { + return getEithers(Converters.passthrough(byte[].class), BukkitConverters.getWrappedSignatureConverter()); + } + + /** + * @return read/writer structure direct access to signature data like chat messages + */ + public StructureModifier getSignatures() { + return structureModifier.withType( + MinecraftReflection.getSaltedSignatureClass(), + BukkitConverters.getWrappedSignatureConverter() + ); + } + + /** + * @param leftConverter converter for left values + * @param rightConverter converter for right values + * @return ProtocolLib's read/write structure for Mojang either structures + * @param left data type after converting from NMS + * @param right data type after converting from NMS + */ + public StructureModifier> getEithers(EquivalentConverter leftConverter, + EquivalentConverter rightConverter) { + return structureModifier.withType( + com.mojang.datafixers.util.Either.class, + BukkitConverters.getEitherConverter( + leftConverter, rightConverter + ) + ); + } + /** * Retrieve a read/write structure for the Map class. * @param keyConverter Converter for map keys diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index ef9a033b..31f2aaed 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -34,10 +34,7 @@ import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.FieldAccessor; import com.comphenix.protocol.reflect.accessors.MethodAccessor; -import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; -import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; -import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers; -import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.reflect.fuzzy.*; import com.comphenix.protocol.wrappers.EnumWrappers; import org.bukkit.Bukkit; @@ -1499,6 +1496,31 @@ public final class MinecraftReflection { return getMinecraftClass("world.entity.player.ProfilePublicKey"); } + public static Class getSaltedSignatureClass() { + try { + return getMinecraftClass("SaltedSignature"); + } catch (RuntimeException runtimeException) { + Class messageSigClass = getMinecraftClass("network.chat.MessageSignature", "MessageSignature"); + + FuzzyClassContract signatureContract = FuzzyClassContract.newBuilder(). + constructor(FuzzyMethodContract.newBuilder(). + parameterCount(2). + parameterSuperOf(Long.TYPE, 0). + parameterSuperOf(byte[].class, 1). + build() + ).build(); + + FuzzyFieldContract fuzzyFieldContract = FuzzyFieldContract.newBuilder(). + typeMatches(getMinecraftObjectMatcher().and(signatureContract)). + build(); + + Class signatureClass = FuzzyReflection.fromClass(messageSigClass, true) + .getField(fuzzyFieldContract) + .getType(); + return setMinecraftClass("SaltedSignature", signatureClass); + } + } + public static Class getProfilePublicKeyDataClass() { return getProfilePublicKeyClass().getClasses()[0]; } diff --git a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index ebc11ab4..ac21d9fc 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -16,6 +16,8 @@ */ package com.comphenix.protocol.wrappers; +import com.comphenix.protocol.wrappers.Either.Left; +import com.comphenix.protocol.wrappers.Either.Right; import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData; import java.lang.ref.WeakReference; import java.lang.reflect.*; @@ -407,6 +409,43 @@ public class BukkitConverters { }); } + + /** + * @param leftConverter convert the left value if available + * @param rightConverter convert the right value if available + * @return converter for Mojang either class + * @param converted left type + * @param converted right type + */ + public static EquivalentConverter> getEitherConverter(EquivalentConverter leftConverter, + EquivalentConverter rightConverter) { + return ignoreNull(new EquivalentConverter>() { + @Override + public Object getGeneric(Either specific) { + return specific.map( + left -> com.mojang.datafixers.util.Either.left(leftConverter.getGeneric(left)), + right -> com.mojang.datafixers.util.Either.right(rightConverter.getGeneric(right)) + ); + } + + @Override + public Either getSpecific(Object generic) { + com.mojang.datafixers.util.Either mjEither = (com.mojang.datafixers.util.Either) generic; + + return mjEither.map( + left -> new Left<>(leftConverter.getSpecific(left)), + right -> new Right<>(rightConverter.getSpecific(right)) + ); + } + + @Override + public Class> getSpecificType() { + Class dummy = Either.class; + return (Class>) dummy; + } + }); + } + /** * Retrieve an equivalent converter for a set of generic items. * @param Element type @@ -564,6 +603,13 @@ public class BukkitConverters { return ignoreNull(handle(WrappedProfileKeyData::getHandle, WrappedProfileKeyData::new, WrappedProfileKeyData.class)); } + /** + * @return converter for cryptographic signature data that are used in login and chat packets + */ + public static EquivalentConverter getWrappedSignatureConverter() { + return ignoreNull(handle(WrappedSaltedSignature::getHandle, WrappedSaltedSignature::new, WrappedSaltedSignature.class)); + } + /** * Retrieve a converter for watchable objects and the respective wrapper. * @return A watchable object converter. diff --git a/src/main/java/com/comphenix/protocol/wrappers/Either.java b/src/main/java/com/comphenix/protocol/wrappers/Either.java new file mode 100644 index 00000000..3400da1f --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/Either.java @@ -0,0 +1,100 @@ +package com.comphenix.protocol.wrappers; + +import java.util.Optional; +import java.util.function.Function; + +/** + * Represents a datatype where either left or right is present. The values are available with a xor semantic. So at + * most and at least one value will be available. + * + * @param left data type + * @param right data type + */ +public abstract class Either { + + public static class Left extends Either { + + private final L value; + + protected Left(L value) { + this.value = value; + } + + @Override + public T map(Function leftConsumer, Function rightConsumer) { + return leftConsumer.apply(value); + } + + @Override + public Optional left() { + return Optional.ofNullable(value); + } + + @Override + public Optional right() { + return Optional.empty(); + } + } + + public static class Right extends Either { + + private final R value; + + protected Right(R value) { + this.value = value; + } + + @Override + public T map(Function leftConsumer, Function rightConsumer) { + return rightConsumer.apply(value); + } + + @Override + public Optional left() { + return Optional.empty(); + } + + @Override + public Optional right() { + return Optional.ofNullable(value); + } + } + + /** + * @param leftConsumer transformer if the left value is present + * @param rightConsumer transformer if the right value is present + * @return result of applying the given functions to the left or right side + * @param result data type of both transformers + */ + public abstract T map(Function leftConsumer, Function rightConsumer); + + /** + * @return left value if present + */ + public abstract Optional left(); + + /** + * @return right value if present + */ + public abstract Optional right(); + + /** + * @param value containing value + * @return either containing a left value + * @param data type of the containing value + * @param right data type + */ + public static Either left(L value) { + return new Left<>(value); + } + + /** + * @param value containing value + * @return either containing a right value + * @param left data type + * @param data type of the containing value + */ + public static Either right(R value) { + return new Right<>(value); + } +} diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedSaltedSignature.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedSaltedSignature.java new file mode 100644 index 00000000..1b8c4746 --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedSaltedSignature.java @@ -0,0 +1,90 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.primitives.Longs; + +/** + * Wrapper representing the signature data associated to signed data by the player. This includes signed chat messages + * and login encryption acknowledgments. + */ +public class WrappedSaltedSignature extends AbstractWrapper { + + private static ConstructorAccessor CONSTRUCTOR; + + private final StructureModifier modifier; + + /** + * Construct a wrapper from a NMS handle + * + * @param handle NMS Signature object + */ + public WrappedSaltedSignature(Object handle) { + super(MinecraftReflection.getSaltedSignatureClass()); + + this.setHandle(handle); + this.modifier = new StructureModifier<>(MinecraftReflection.getSaltedSignatureClass()).withTarget(handle); + } + + /** + * Construct a wrapper and NMS handle containing the given values + * @param salt salt/nonce for this signature + * @param signature binary cryptographic signature + */ + public WrappedSaltedSignature(long salt, byte[] signature) { + super(MinecraftReflection.getSaltedSignatureClass()); + + if (CONSTRUCTOR == null) { + CONSTRUCTOR = Accessors.getConstructorAccessor( + this.getHandleType(), + Long.TYPE, byte[].class); + } + + this.setHandle(CONSTRUCTOR.invoke(salt, signature)); + this.modifier = new StructureModifier<>(MinecraftReflection.getSaltedSignatureClass()).withTarget(this.handle); + } + + /** + * @return if a cryptographic signature data is present + */ + public boolean isSigned() { + return getSignature().length > 0; + } + + /** + * @return cryptographic salt/nonce + */ + public long getSalt() { + return (long) modifier.withType(Long.TYPE).read(0); + } + + /** + * @param salt cryptographic salt/nonce + */ + public void setSalt(long salt) { + modifier.withType(Long.TYPE).write(0, salt); + } + + /** + * @return binary signature data associated to the salt and message + */ + public byte[] getSignature() { + return modifier.withType(byte[].class).read(0); + } + + /** + * @param signature binary signature data associated to the salt and message + */ + public void setSignature(byte[] signature) { + modifier.withType(byte[].class).write(0, signature); + } + + /** + * @return the long salt represented in 8 bytes + */ + public byte[] getSaltBytes() { + return Longs.toByteArray(getSalt()); + } +} diff --git a/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index 9839fd33..4792bafc 100644 --- a/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -39,6 +39,7 @@ import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.BlockPosition; import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.ComponentConverter; +import com.comphenix.protocol.wrappers.Either; import com.comphenix.protocol.wrappers.EnumWrappers; import com.comphenix.protocol.wrappers.EnumWrappers.EntityUseAction; import com.comphenix.protocol.wrappers.EnumWrappers.Hand; @@ -52,6 +53,7 @@ import com.comphenix.protocol.wrappers.WrappedDataWatcher.Registry; import com.comphenix.protocol.wrappers.WrappedDataWatcher.WrappedDataWatcherObject; import com.comphenix.protocol.wrappers.WrappedEnumEntityUseAction; import com.comphenix.protocol.wrappers.WrappedGameProfile; +import com.comphenix.protocol.wrappers.WrappedSaltedSignature; import com.comphenix.protocol.wrappers.WrappedRegistry; import com.comphenix.protocol.wrappers.WrappedWatchableObject; import com.comphenix.protocol.wrappers.nbt.NbtCompound; @@ -707,6 +709,46 @@ public class PacketContainerTest { assertArrayEquals(components, back); } + @Test + public void testLoginSignatureNonce() { + PacketContainer encryptionStart = new PacketContainer(PacketType.Login.Client.ENCRYPTION_BEGIN); + encryptionStart.getByteArrays().write(0, new byte[]{1, 2, 3}); + + byte[] nonce = {4, 5, 6}; + encryptionStart.getLoginSignatures().write(0, Either.left(nonce)); + + byte[] read = encryptionStart.getLoginSignatures().read(0).left().get(); + assertArrayEquals(nonce, read); + } + + @Test + public void testLoginSignatureSigned() { + PacketContainer encryptionStart = new PacketContainer(PacketType.Login.Client.ENCRYPTION_BEGIN); + encryptionStart.getByteArrays().write(0, new byte[]{1, 2, 3}); + + byte[] signature = new byte[512]; + long salt = 124L; + encryptionStart.getLoginSignatures().write(0, Either.right(new WrappedSaltedSignature(salt, signature))); + + WrappedSaltedSignature read = encryptionStart.getLoginSignatures().read(0).right().get(); + assertEquals(salt, read.getSalt()); + assertArrayEquals(signature, read.getSignature()); + } + + @Test + public void testSignedChatMessage() { + PacketContainer chatPacket = new PacketContainer(PacketType.Play.Client.CHAT); + + byte[] signature = new byte[512]; + long salt = 124L; + WrappedSaltedSignature wrappedSignature = new WrappedSaltedSignature(salt, signature); + chatPacket.getSignatures().write(0, wrappedSignature); + + WrappedSaltedSignature read = chatPacket.getSignatures().read(0); + assertEquals(salt, read.getSalt()); + assertArrayEquals(signature, read.getSignature()); + } + private void assertPacketsEqual(PacketContainer constructed, PacketContainer cloned) { StructureModifier firstMod = constructed.getModifier(), secondMod = cloned.getModifier(); assertEquals(firstMod.size(), secondMod.size()); diff --git a/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java b/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java index 2596c1a9..598b5508 100644 --- a/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java +++ b/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java @@ -17,6 +17,7 @@ import net.minecraft.network.protocol.game.PacketPlayOutUpdateAttributes; import net.minecraft.network.protocol.status.ServerPing; import net.minecraft.network.syncher.DataWatcher; import net.minecraft.server.network.PlayerConnection; +import net.minecraft.util.MinecraftEncryption; import net.minecraft.world.level.ChunkCoordIntPair; import net.minecraft.world.level.block.state.IBlockData; import org.bukkit.Material; @@ -119,6 +120,11 @@ public class MinecraftReflectionTest { assertEquals(DataWatcher.Item.class, MinecraftReflection.getDataWatcherItemClass()); } + @Test + public void testLoginSignature() { + assertEquals(MinecraftEncryption.b.class, MinecraftReflection.getSaltedSignatureClass()); + } + @Test public void testItemStacks() { ItemStack stack = new ItemStack(Material.GOLDEN_SWORD); diff --git a/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java b/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java index a978a49c..1ac1dd57 100644 --- a/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java +++ b/src/test/java/com/comphenix/protocol/wrappers/BukkitConvertersTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.comphenix.protocol.BukkitInitialization; import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.wrappers.Either.Left; + import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; @@ -38,4 +40,19 @@ public class BukkitConvertersTest { assertEquals(item.hasItemMeta(), back.hasItemMeta()); assertTrue(Bukkit.getItemFactory().equals(item.getItemMeta(), back.getItemMeta())); } + + @Test + public void testEither() { + Either test = new Left<>("bla"); + + EquivalentConverter> converter = BukkitConverters.getEitherConverter( + Converters.passthrough(String.class), Converters.passthrough(String.class) + ); + + com.mojang.datafixers.util.Either nmsEither = (com.mojang.datafixers.util.Either) converter.getGeneric(test); + Either wrapped = converter.getSpecific(nmsEither); + + assertEquals(wrapped.left(), nmsEither.left()); + assertEquals(wrapped.right(), nmsEither.right()); + } } diff --git a/src/test/java/com/comphenix/protocol/wrappers/EitherTest.java b/src/test/java/com/comphenix/protocol/wrappers/EitherTest.java new file mode 100644 index 00000000..44293e4e --- /dev/null +++ b/src/test/java/com/comphenix/protocol/wrappers/EitherTest.java @@ -0,0 +1,35 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.wrappers.Either.Left; +import com.comphenix.protocol.wrappers.Either.Right; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EitherTest { + + @Test + void testLeft() { + Left left = new Left<>("left"); + + assertEquals(left.left(), Optional.of("left")); + assertEquals(left.right(), Optional.empty()); + + String map = left.map(l -> l + "left", r -> r + "right"); + assertEquals("leftleft", map); + } + + @Test + void testRight() { + Right right = new Right<>("right"); + + assertEquals(right.left(), Optional.empty()); + assertEquals(right.right(), Optional.of("right")); + + String map = right.map(l -> l + "left", r -> r + "right"); + assertEquals("rightright", map); + } +} diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedSaltedSignatureTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedSaltedSignatureTest.java new file mode 100644 index 00000000..92fd7bee --- /dev/null +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedSaltedSignatureTest.java @@ -0,0 +1,87 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.BukkitInitialization; + +import java.util.concurrent.ThreadLocalRandom; + +import net.minecraft.util.MinecraftEncryption; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class WrappedSaltedSignatureTest { + + @BeforeAll + static void initializeBukkit() { + BukkitInitialization.initializeAll(); + } + + @Test + void testLoginSignature() { + long salt = ThreadLocalRandom.current().nextLong(); + byte[] signature = new byte[512]; + ThreadLocalRandom.current().nextBytes(signature); + + // test key data conversion + WrappedSaltedSignature loginSignature = new WrappedSaltedSignature(salt, signature); + + Object handle = loginSignature.getHandle(); + MinecraftEncryption.b data = assertInstanceOf(MinecraftEncryption.b.class, handle); + + assertTrue(data.a()); + assertArrayEquals(signature, data.d()); + assertEquals(salt, data.c()); + + // test key data unwrapping + WrappedSaltedSignature unwrapped = BukkitConverters.getWrappedSignatureConverter().getSpecific(data); + assertNotNull(unwrapped); + assertTrue(unwrapped.isSigned()); + assertEquals(loginSignature.getSalt(), unwrapped.getSalt()); + assertArrayEquals(loginSignature.getSignature(), unwrapped.getSignature()); + assertArrayEquals(loginSignature.getSaltBytes(), unwrapped.getSaltBytes()); + + // test key data wrapping + Object wrappedData = BukkitConverters.getWrappedSignatureConverter().getGeneric(loginSignature); + MinecraftEncryption.b wrapped = assertInstanceOf(MinecraftEncryption.b.class, wrappedData); + + assertTrue(wrapped.a()); + assertEquals(loginSignature.getSalt(), wrapped.c()); + assertArrayEquals(loginSignature.getSignature(), wrapped.d()); + assertArrayEquals(loginSignature.getSaltBytes(), wrapped.b()); + } + + @Test + void testSignedMessageWithoutSignature() { + long salt = ThreadLocalRandom.current().nextLong(); + byte[] signature = {}; + + // test key data conversion + WrappedSaltedSignature loginSignature = new WrappedSaltedSignature(salt, signature); + + Object handle = loginSignature.getHandle(); + MinecraftEncryption.b data = assertInstanceOf(MinecraftEncryption.b.class, handle); + + assertFalse(data.a()); + assertArrayEquals(signature, data.d()); + assertEquals(salt, data.c()); + + // test key data unwrapping + WrappedSaltedSignature unwrapped = BukkitConverters.getWrappedSignatureConverter().getSpecific(data); + assertNotNull(unwrapped); + assertFalse(unwrapped.isSigned()); + assertEquals(loginSignature.getSalt(), unwrapped.getSalt()); + assertArrayEquals(loginSignature.getSignature(), unwrapped.getSignature()); + assertArrayEquals(loginSignature.getSaltBytes(), unwrapped.getSaltBytes()); + + // test key data wrapping + Object wrappedData = BukkitConverters.getWrappedSignatureConverter().getGeneric(loginSignature); + MinecraftEncryption.b wrapped = assertInstanceOf(MinecraftEncryption.b.class, wrappedData); + + assertFalse(wrapped.a()); + assertEquals(loginSignature.getSalt(), wrapped.c()); + assertArrayEquals(loginSignature.getSignature(), wrapped.d()); + assertArrayEquals(loginSignature.getSaltBytes(), wrapped.b()); + } +}