From 37a20964c3dc041859dede305f1ac2385fe5557a Mon Sep 17 00:00:00 2001 From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:17:50 -0500 Subject: [PATCH] Push initial datacomponent obfuscation logic This general principal is that there is no mutation of the patched map, only swapping of the valid values. Additional work will need to be done to support hiding enchantments whilst keeping the glint, as we either have to append an enchantment, or need to add a glint override.. which kinda defeats the purpose above. Cant add a vanilla enchantment because those can be removed! Further investigation needed. --- .../component/DataComponentType.java.patch | 25 ++++ .../core/component/DataComponents.java.patch | 24 ---- .../minecraft/world/item/ItemStack.java.patch | 5 +- .../paper/configuration/Configurations.java | 8 +- .../configuration/GlobalConfiguration.java | 72 ++++++++++++ .../configuration/PaperConfigurations.java | 14 ++- .../configuration/WorldConfiguration.java | 11 -- .../paper/util/DataSanitizationUtil.java | 108 ++++++++++++++++-- 8 files changed, 213 insertions(+), 54 deletions(-) create mode 100644 paper-server/patches/sources/net/minecraft/core/component/DataComponentType.java.patch delete mode 100644 paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch diff --git a/paper-server/patches/sources/net/minecraft/core/component/DataComponentType.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponentType.java.patch new file mode 100644 index 0000000000..7f5323a432 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/core/component/DataComponentType.java.patch @@ -0,0 +1,25 @@ +--- a/net/minecraft/core/component/DataComponentType.java ++++ b/net/minecraft/core/component/DataComponentType.java +@@ -80,11 +_,11 @@ + static class SimpleType implements DataComponentType { + @Nullable + private final Codec codec; +- private final StreamCodec streamCodec; ++ private final java.util.function.Supplier> streamCodec; // Paper + + SimpleType(@Nullable Codec codec, StreamCodec streamCodec) { + this.codec = codec; +- this.streamCodec = streamCodec; ++ this.streamCodec = com.google.common.base.Suppliers.memoize(() -> io.papermc.paper.util.DataSanitizationUtil.get(this, streamCodec)); // Paper + } + + @Nullable +@@ -95,7 +_,7 @@ + + @Override + public StreamCodec streamCodec() { +- return this.streamCodec; ++ return this.streamCodec.get(); // Paper + } + + @Override diff --git a/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch deleted file mode 100644 index c1f061a0af..0000000000 --- a/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch +++ /dev/null @@ -1,24 +0,0 @@ ---- a/net/minecraft/core/component/DataComponents.java -+++ b/net/minecraft/core/component/DataComponents.java -@@ -180,10 +_,10 @@ - "map_post_processing", builder -> builder.networkSynchronized(MapPostProcessing.STREAM_CODEC) - ); - public static final DataComponentType CHARGED_PROJECTILES = register( -- "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(ChargedProjectiles.STREAM_CODEC).cacheEncoding() -+ "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CHARGED_PROJECTILES).cacheEncoding() // Paper - sanitize charged projectiles - ); - public static final DataComponentType BUNDLE_CONTENTS = register( -- "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(BundleContents.STREAM_CODEC).cacheEncoding() -+ "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.BUNDLE_CONTENTS).cacheEncoding() // Paper - sanitize bundle contents - ); - public static final DataComponentType POTION_CONTENTS = register( - "potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding() -@@ -250,7 +_,7 @@ - "pot_decorations", builder -> builder.persistent(PotDecorations.CODEC).networkSynchronized(PotDecorations.STREAM_CODEC).cacheEncoding() - ); - public static final DataComponentType CONTAINER = register( -- "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(ItemContainerContents.STREAM_CODEC).cacheEncoding() -+ "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CONTAINER).cacheEncoding() // Paper - sanitize container contents - ); - public static final DataComponentType BLOCK_STATE = register( - "block_state", builder -> builder.persistent(BlockItemStateProperties.CODEC).networkSynchronized(BlockItemStateProperties.STREAM_CODEC).cacheEncoding() diff --git a/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch index 2bd24dd054..d2ba980157 100644 --- a/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch @@ -21,14 +21,15 @@ + if (value.isEmpty() || value.getItem() == null) { // CraftBukkit - NPE fix itemstack.getItem() buffer.writeVarInt(0); } else { - buffer.writeVarInt(value.getCount()); +- buffer.writeVarInt(value.getCount()); ++ buffer.writeVarInt(io.papermc.paper.util.DataSanitizationUtil.sanitizeCount(value, value.getCount())); // Paper ITEM_STREAM_CODEC.encode(buffer, value.getItemHolder()); + // Spigot start - filter + // value = value.copy(); + // CraftItemStack.setItemMeta(value, CraftItemStack.getItemMeta(value)); // Paper - This is no longer with raw NBT being handled in metadata + // Paper start - adventure; conditionally render translatable components + boolean prev = net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.get(); -+ try { ++ try (io.papermc.paper.util.DataSanitizationUtil.SafeAutoClosable unused = io.papermc.paper.util.DataSanitizationUtil.passContext(value)) { + net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(true); DataComponentPatch.STREAM_CODEC.encode(buffer, value.components.asPatch()); + } finally { diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java index 109e569b7b..4a9258b62d 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java @@ -80,7 +80,7 @@ public abstract class Configurations { } @MustBeInvokedByOverriders - protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() { + protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(RegistryAccess registryAccess) { return this.createLoaderBuilder(); } @@ -104,7 +104,7 @@ public abstract class Configurations { } public G initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException { - return this.initializeGlobalConfiguration(creator(this.globalConfigClass, true)); + return this.initializeGlobalConfiguration(registryAccess, creator(this.globalConfigClass, true)); } private void trySaveFileNode(YamlConfigurationLoader loader, ConfigurationNode node, String filename) throws ConfigurateException { @@ -117,9 +117,9 @@ public abstract class Configurations { } } - protected G initializeGlobalConfiguration(final CheckedFunction creator) throws ConfigurateException { + protected G initializeGlobalConfiguration(final RegistryAccess registryAccess, final CheckedFunction creator) throws ConfigurateException { final Path configFile = this.globalFolder.resolve(this.globalConfigFileName); - final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder() + final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder(registryAccess) .defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build())) .path(configFile) .build(); diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index 088b8fe5d1..dde005a228 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -7,8 +7,13 @@ import io.papermc.paper.configuration.type.number.DoubleOr; import io.papermc.paper.configuration.type.number.IntOr; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.Util; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.DataComponents; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @@ -16,10 +21,13 @@ import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.PostProcess; import org.spongepowered.configurate.objectmapping.meta.Required; import org.spongepowered.configurate.objectmapping.meta.Setting; +import org.spongepowered.configurate.serialize.SerializationException; +import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.OptionalInt; +import java.util.Set; @SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) public class GlobalConfiguration extends ConfigurationPart { @@ -354,4 +362,68 @@ public class GlobalConfiguration extends ConfigurationPart { public boolean disableChorusPlantUpdates = false; public boolean disableMushroomBlockUpdates = false; } + + public Anticheat anticheat; + + public class Anticheat extends ConfigurationPart { + + public Obfuscation obfuscation; + + public class Obfuscation extends ConfigurationPart { + public Items items = new Items(); + + public class Items extends ConfigurationPart { + public static Set> OVERRIDEN_TYPES = new HashSet<>(); + + public boolean enableItemObfuscation = false; + public AssetObfuscationConfiguration allModels = new AssetObfuscationConfiguration(true, + Set.of( + DataComponents.MAX_STACK_SIZE, + DataComponents.MAX_DAMAGE, + DataComponents.DAMAGE, + DataComponents.UNBREAKABLE, + DataComponents.CUSTOM_NAME, + DataComponents.ITEM_NAME, + DataComponents.LORE, + DataComponents.RARITY, + DataComponents.ENCHANTMENTS, + DataComponents.ATTRIBUTE_MODIFIERS, + DataComponents.CUSTOM_DATA, + DataComponents.WRITABLE_BOOK_CONTENT, + DataComponents.WRITTEN_BOOK_CONTENT, + DataComponents.MAP_ID, + DataComponents.MAP_DECORATIONS, + DataComponents.MAP_POST_PROCESSING, + DataComponents.LODESTONE_TRACKER + ) + ); + public Map modelOverrides = Map.of( + net.minecraft.world.item.Items.ELYTRA.components().get(DataComponents.ITEM_MODEL).toString(), new AssetObfuscationConfiguration( + true, Util.make(new HashSet<>(), (obj) -> { + obj.addAll(allModels.sanitizedComponents()); + obj.remove(DataComponents.DAMAGE); // don't sanitize damage for elytra + } + ) + )); + + public AssetObfuscationConfiguration getAssetObfuscation(ItemStack resourceLocation) { + return this.modelOverrides.getOrDefault(resourceLocation.get(DataComponents.ITEM_MODEL).toString(), this.allModels); // todo make optimized resourcelocation lookup + } + + @ConfigSerializable + public record AssetObfuscationConfiguration(boolean sanitizeCount, Set> sanitizedComponents) { + } + + @PostProcess + public void computeOverridenTypes() { + OVERRIDEN_TYPES.addAll(this.allModels.sanitizedComponents); + for (AssetObfuscationConfiguration configuration : this.modelOverrides.values()) { + OVERRIDEN_TYPES.addAll(configuration.sanitizedComponents); + } + } + } + + } + + } } diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java index 098ab351de..af9ce08f4f 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java @@ -48,6 +48,7 @@ import java.util.List; import java.util.function.Function; import java.util.function.Supplier; import net.minecraft.core.RegistryAccess; +import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; @@ -193,16 +194,17 @@ public class PaperConfigurations extends Configurations defaultGlobalOptions(registryAccess, options)); } - private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options) { + private static ConfigurationOptions defaultGlobalOptions(RegistryAccess registryAccess, ConfigurationOptions options) { return options .header(GLOBAL_HEADER) .serializers(builder -> builder .register(new PacketClassSerializer()) + .register(new RegistryValueSerializer<>(new TypeToken>() {}, registryAccess, Registries.DATA_COMPONENT_TYPE, false)) ); } @@ -316,7 +318,7 @@ public class PaperConfigurations extends Configurations builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer())); return BasicConfigurationNode.root(options); } diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java index b1c917d650..845b0ac8b1 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java @@ -88,17 +88,6 @@ public class WorldConfiguration extends ConfigurationPart { public class Anticheat extends ConfigurationPart { - public Obfuscation obfuscation; - - public class Obfuscation extends ConfigurationPart { - public Items items = new Items(); - public class Items extends ConfigurationPart { - public boolean hideItemmeta = false; - public boolean hideDurability = false; - public boolean hideItemmetaWithVisualEffects = false; - } - } - public AntiXray antiXray; public class AntiXray extends ConfigurationPart { diff --git a/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java b/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java index 72483dedd3..91afe7001a 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java +++ b/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java @@ -1,19 +1,34 @@ package io.papermc.paper.util; +import java.io.Closeable; +import java.io.IOException; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; +import com.google.common.base.Preconditions; +import io.papermc.paper.configuration.GlobalConfiguration; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.DataComponents; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.component.BundleContents; import net.minecraft.world.item.component.ChargedProjectiles; import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.item.component.LodestoneTracker; +import net.minecraft.world.item.enchantment.ItemEnchantments; +import net.minecraft.world.level.Level; import org.apache.commons.lang3.math.Fraction; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) @@ -29,9 +44,41 @@ public final class DataSanitizationUtil { return sanitizer; } - public static final StreamCodec CHARGED_PROJECTILES = codec(ChargedProjectiles.STREAM_CODEC, DataSanitizationUtil::sanitizeChargedProjectiles); - public static final StreamCodec BUNDLE_CONTENTS = codec(BundleContents.STREAM_CODEC, DataSanitizationUtil::sanitizeBundleContents); - public static final StreamCodec CONTAINER = codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY); + public static SafeAutoClosable passContext(final ItemStack itemStack) { + final DataSanitizer sanitizer = DATA_SANITIZER.get(); + if (!sanitizer.isSanitizing()) { + return () -> {}; // Dont pass any context + } + + return new ContentScope(sanitizer.scope.get(), itemStack); + } + + + public static final Map, StreamCodec> BUILT_IN_CODEC_OVERRIDES = Map.of( + DataComponents.CHARGED_PROJECTILES, codec(ChargedProjectiles.STREAM_CODEC, DataSanitizationUtil::sanitizeChargedProjectiles), + DataComponents.BUNDLE_CONTENTS, codec(BundleContents.STREAM_CODEC, DataSanitizationUtil::sanitizeBundleContents), + DataComponents.CONTAINER, codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY) + ); + + public static final Map, Object> EMPTY_MAP = Map.of( + DataComponents.LODESTONE_TRACKER, new LodestoneTracker(Optional.empty(), false) + ); + + + public static StreamCodec get(DataComponentType componentType, StreamCodec vanillaCodec) { + // First check do we have our built in overrides, if so, override it. + if (BUILT_IN_CODEC_OVERRIDES.containsKey(componentType)) { + return (StreamCodec) BUILT_IN_CODEC_OVERRIDES.get(componentType); + } + + // Now check the obfuscation, where we lookup if the type is overriden in any of the configurations, if so, wrap the codec + GlobalConfiguration.Anticheat.Obfuscation.Items obfuscation = GlobalConfiguration.get().anticheat.obfuscation.items; + if (obfuscation.enableItemObfuscation && GlobalConfiguration.Anticheat.Obfuscation.Items.OVERRIDEN_TYPES.contains(componentType)) { + return new DataSanitizationCodec<>(vanillaCodec, new DefaultDataComponentSanitizer<>(componentType, EMPTY_MAP.get(componentType))); + } + + return vanillaCodec; + } private static ChargedProjectiles sanitizeChargedProjectiles(final ChargedProjectiles projectiles) { if (projectiles.isEmpty()) { @@ -65,11 +112,37 @@ public final class DataSanitizationUtil { // Ensure that potentially overstacked bundles are not represented by empty (count=0) itemstacks. return new BundleContents(sanitizedRepresentation); } - private static StreamCodec codec(final StreamCodec delegate, final UnaryOperator sanitizer) { return new DataSanitizationCodec<>(delegate, sanitizer); } + public static int sanitizeCount(ItemStack itemStack, int count) { + GlobalConfiguration.Anticheat.Obfuscation.Items items = GlobalConfiguration.get().anticheat.obfuscation.items; + if (items.enableItemObfuscation && items.getAssetObfuscation(itemStack).sanitizeCount()) { + return 1; + } else { + return count; + } + } + + + private record DefaultDataComponentSanitizer(DataComponentType type, @Nullable Object override) implements UnaryOperator { + + @Override + public A apply(final A oldvalue) { + ContentScope scope = DATA_SANITIZER.get().scope.get(); + ItemStack targetItemstack = scope.itemStack; + // Does this asset override this component? If not, return oldValue. + if (!GlobalConfiguration.get().anticheat.obfuscation.items.getAssetObfuscation(targetItemstack).sanitizedComponents().contains(this.type)) { + return oldvalue; + } + + // We don't need to check if its overriden because we KNOW it is if we are serializing it over to the client. + + return (A) scope.itemStack.getPrototype().getOrDefault(this.type, this.override == null ? oldvalue : this.override); + } + } + private record DataSanitizationCodec(StreamCodec delegate, UnaryOperator sanitizer) implements StreamCodec { @Override @@ -79,7 +152,7 @@ public final class DataSanitizationUtil { @Override public void encode(final @NonNull B buf, final @NonNull A value) { - if (!DATA_SANITIZER.get().value().get()) { + if (!DATA_SANITIZER.get().isSanitizing()) { this.delegate.encode(buf, value); } else { this.delegate.encode(buf, this.sanitizer.apply(value)); @@ -87,10 +160,10 @@ public final class DataSanitizationUtil { } } - public record DataSanitizer(AtomicBoolean value) implements AutoCloseable { + public record DataSanitizer(AtomicBoolean value, AtomicReference scope) implements SafeAutoClosable { public DataSanitizer() { - this(new AtomicBoolean(false)); + this(new AtomicBoolean(false), new AtomicReference<>()); } public void start() { @@ -101,6 +174,27 @@ public final class DataSanitizationUtil { public void close() { this.value.compareAndSet(true, false); } + + public boolean isSanitizing() { + return value().get(); + } + } + + public record ContentScope(@Nullable ContentScope oldScope, ItemStack itemStack) implements SafeAutoClosable { + public ContentScope { + Preconditions.checkNotNull(DATA_SANITIZER.get(), "Expected data santizier content available"); + } + + @Override + public void close() { + DATA_SANITIZER.get().scope().set(this.oldScope); + } + } + + public interface SafeAutoClosable extends AutoCloseable { + + @Override + void close(); } private DataSanitizationUtil() {