added all the code.

This commit is contained in:
2026-04-06 15:23:47 -05:00
parent 090c486f31
commit ea4f63180d
15 changed files with 491 additions and 172 deletions

View File

@@ -0,0 +1,58 @@
package com.example.signleakshield;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class ExploitState {
public static final Map<BlockPos, CapturedSignData> SIGNS = new ConcurrentHashMap<>();
public static volatile ForcedOpenContext pendingForcedOpen;
private ExploitState() {
}
public static void rememberForcedOpen(BlockPos pos, boolean front) {
pendingForcedOpen = new ForcedOpenContext(pos.toImmutable(), front, System.currentTimeMillis());
}
public static void clearForcedOpen() {
pendingForcedOpen = null;
}
public record ForcedOpenContext(BlockPos pos, boolean front, long timeMs) {
}
public static final class CapturedSignData {
private final Text[] front;
private final Text[] back;
private final boolean suspicious;
public CapturedSignData(Text[] front, Text[] back, boolean suspicious) {
this.front = copy(front);
this.back = copy(back);
this.suspicious = suspicious;
}
public Text[] getFront() {
return copy(front);
}
public Text[] getBack() {
return copy(back);
}
public boolean isSuspicious() {
return suspicious;
}
private static Text[] copy(Text[] source) {
Text[] out = new Text[4];
for (int i = 0; i < 4; i++) {
out[i] = i < source.length && source[i] != null ? source[i] : Text.empty();
}
return out;
}
}
}

View File

@@ -0,0 +1,15 @@
package com.example.signleakshield;
import net.fabricmc.api.ClientModInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class SignLeakShieldClient implements ClientModInitializer {
public static final String MOD_ID = "signleakshield";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
@Override
public void onInitializeClient() {
LOGGER.info("Sign Leak Shield initialized");
}
}

View File

@@ -0,0 +1,53 @@
package com.example.signleakshield;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtList;
import net.minecraft.text.Text;
public final class SignTextExtractor {
private SignTextExtractor() {
}
public static ExploitState.CapturedSignData fromNbt(NbtCompound nbt) {
Text[] front = readSide(nbt, "front_text");
Text[] back = readSide(nbt, "back_text");
boolean suspicious = containsSuspicious(front) || containsSuspicious(back);
return new ExploitState.CapturedSignData(front, back, suspicious);
}
private static boolean containsSuspicious(Text[] lines) {
for (Text line : lines) {
if (TextSanitizer.isSuspicious(line)) {
return true;
}
}
return false;
}
private static Text[] readSide(NbtCompound root, String sideKey) {
Text[] lines = new Text[] { Text.empty(), Text.empty(), Text.empty(), Text.empty() };
if (!root.contains(sideKey, NbtElement.COMPOUND_TYPE)) {
return lines;
}
NbtCompound side = root.getCompound(sideKey);
if (!side.contains("messages", NbtElement.LIST_TYPE)) {
return lines;
}
NbtList messages = side.getList("messages", NbtElement.STRING_TYPE);
for (int i = 0; i < 4 && i < messages.size(); i++) {
String raw = messages.getString(i);
try {
Text parsed = Text.Serialization.fromJson(raw, null);
lines[i] = parsed != null ? parsed : Text.literal(raw);
} catch (Throwable ignored) {
lines[i] = Text.literal(raw);
}
}
return lines;
}
}

View File

@@ -0,0 +1,65 @@
package com.example.signleakshield;
import net.minecraft.text.KeybindTextContent;
import net.minecraft.text.PlainTextContent;
import net.minecraft.text.Text;
import net.minecraft.text.TextContent;
import net.minecraft.text.TranslatableTextContent;
public final class TextSanitizer {
private TextSanitizer() {
}
public static boolean isSuspicious(Text text) {
if (text == null) {
return false;
}
TextContent content = text.getContent();
if (content instanceof TranslatableTextContent || content instanceof KeybindTextContent) {
return true;
}
for (Text sibling : text.getSiblings()) {
if (isSuspicious(sibling)) {
return true;
}
}
return false;
}
public static String sanitize(Text text) {
StringBuilder out = new StringBuilder();
append(text, out);
return out.toString();
}
private static void append(Text text, StringBuilder out) {
if (text == null) {
return;
}
TextContent content = text.getContent();
if (content instanceof PlainTextContent plain) {
out.append(plain.string());
} else if (content instanceof TranslatableTextContent translatable) {
String fallback = translatable.getFallback();
if (fallback != null && !fallback.isEmpty()) {
out.append(fallback);
} else {
out.append(translatable.getKey());
}
} else if (content instanceof KeybindTextContent keybind) {
out.append(keybind.getKey());
} else {
out.append(text.getString());
}
for (Text sibling : text.getSiblings()) {
append(sibling, out);
}
}
}

View File

@@ -0,0 +1,56 @@
package com.example.signleakshield.mixin;
import com.example.signleakshield.ExploitState;
import com.example.signleakshield.TextSanitizer;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.PacketCallbacks;
import net.minecraft.network.packet.Packet;
import net.minecraft.network.packet.c2s.play.UpdateSignC2SPacket;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(ClientConnection.class)
public abstract class ClientConnectionMixin {
@ModifyVariable(method = "send(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/PacketCallbacks;Z)V", at = @At("HEAD"), argsOnly = true)
private Packet<?> signleakshield$rewriteOutgoing(Packet<?> packet) {
if (!(packet instanceof UpdateSignC2SPacket signPacket)) {
return packet;
}
ExploitState.ForcedOpenContext forcedOpen = ExploitState.pendingForcedOpen;
if (forcedOpen == null) {
return packet;
}
BlockPos pos = signPacket.getPos();
if (!forcedOpen.pos().equals(pos)) {
return packet;
}
if (forcedOpen.front() != signPacket.isFront()) {
return packet;
}
long ageMs = System.currentTimeMillis() - forcedOpen.timeMs();
if (ageMs < 0L || ageMs > 5000L) {
return packet;
}
ExploitState.CapturedSignData captured = ExploitState.SIGNS.get(pos);
if (captured == null || !captured.isSuspicious()) {
return packet;
}
Text[] lines = signPacket.isFront() ? captured.getFront() : captured.getBack();
String line1 = TextSanitizer.sanitize(lines[0]);
String line2 = TextSanitizer.sanitize(lines[1]);
String line3 = TextSanitizer.sanitize(lines[2]);
String line4 = TextSanitizer.sanitize(lines[3]);
ExploitState.clearForcedOpen();
return new UpdateSignC2SPacket(pos, signPacket.isFront(), line1, line2, line3, line4);
}
}

View File

@@ -0,0 +1,29 @@
package com.example.signleakshield.mixin;
import com.example.signleakshield.ExploitState;
import com.example.signleakshield.SignTextExtractor;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket;
import net.minecraft.util.math.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public abstract class ClientPlayNetworkHandlerBlockEntityUpdateMixin {
@Inject(method = "onBlockEntityUpdate", at = @At("HEAD"))
private void signleakshield$captureSign(BlockEntityUpdateS2CPacket packet, CallbackInfo ci) {
if (packet.getNbt() == null) {
return;
}
if (packet.getBlockEntityType() != BlockEntityType.SIGN && packet.getBlockEntityType() != BlockEntityType.HANGING_SIGN) {
return;
}
BlockPos pos = packet.getPos();
ExploitState.SIGNS.put(pos.toImmutable(), SignTextExtractor.fromNbt(packet.getNbt()));
}
}

View File

@@ -0,0 +1,17 @@
package com.example.signleakshield.mixin;
import com.example.signleakshield.ExploitState;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.SignEditorOpenS2CPacket;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public abstract class ClientPlayNetworkHandlerSignEditorOpenMixin {
@Inject(method = "onSignEditorOpen", at = @At("HEAD"))
private void signleakshield$rememberForcedOpen(SignEditorOpenS2CPacket packet, CallbackInfo ci) {
ExploitState.rememberForcedOpen(packet.getPos(), packet.isFront());
}
}

View File

@@ -0,0 +1,30 @@
{
"schemaVersion": 1,
"id": "signleakshield",
"version": "${version}",
"name": "Sign Leak Shield",
"description": "Client-side defensive patch for forced sign-editor translation/keybind leak probes.",
"authors": [
"Omni Systems"
],
"contact": {
"email": "contact@d3lta.one",
"sources": "https://github.com/OusmBlueNinja/SignShild"
},
"license": "MIT",
"environment": "client",
"entrypoints": {
"client": [
"com.example.signleakshield.SignLeakShieldClient"
]
},
"mixins": [
"signleakshield.mixins.json"
],
"depends": {
"fabricloader": ">=0.18.4",
"minecraft": "1.21.1",
"java": ">=21",
"fabric-api": "*"
}
}

View File

@@ -0,0 +1,13 @@
{
"required": true,
"package": "com.example.signleakshield.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"ClientConnectionMixin",
"ClientPlayNetworkHandlerBlockEntityUpdateMixin",
"ClientPlayNetworkHandlerSignEditorOpenMixin"
],
"injectors": {
"defaultRequire": 1
}
}