Skip to content

Commit 1cb9e40

Browse files
committed
Bootstrap Hardening, Authlib Rewrite, New Mixins, 26.2 Compat & More
1 parent b841abd commit 1cb9e40

18 files changed

Lines changed: 519 additions & 345 deletions

README.md

Lines changed: 37 additions & 50 deletions
Large diffs are not rendered by default.

WRITEUP.md

Lines changed: 45 additions & 164 deletions
Large diffs are not rendered by default.

build.gradle

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ if (singleBuild) {
4949

5050
apply plugin: 'base'
5151

52+
tasks.named('clean') {
53+
delete 'build-121', 'build-2612'
54+
}
55+
5256
base {
5357
archivesName = project.archives_base_name
5458
}
@@ -101,6 +105,7 @@ if (singleBuild) {
101105
if (mcVer.startsWith("1.21")) {
102106
srcDir 'src/version-1.21/java'
103107
} else if (mcVer.startsWith("26.")) {
108+
// Shared 26.x source set; compiled against the current 26.x target below.
104109
srcDir 'src/version-26.1/java'
105110
} else {
106111
throw new GradleException("Unsupported minecraft_version: ${mcVer}. Expected 1.21.x or 26.x")
@@ -154,6 +159,7 @@ def multiVersionBuilds = [
154159
loomPluginId : 'fabric-loom',
155160
loomVersion : '1.15.5',
156161
minecraftVersion: '1.21.11',
162+
artifactLabel : '1.21.11',
157163
mappingsMode : 'official',
158164
fabricVersion : '0.141.4+1.21.11',
159165
loaderVersion : '0.18.4',
@@ -162,11 +168,12 @@ def multiVersionBuilds = [
162168
],
163169
[
164170
loomPluginId : 'net.fabricmc.fabric-loom',
165-
loomVersion : '1.16.3',
166-
minecraftVersion: '26.1.2',
167-
fabricVersion : '0.149.1+26.1.2',
168-
loaderVersion : '0.19.2',
169-
modmenuVersion : '18.0.0-beta.1',
171+
loomVersion : '1.17.12',
172+
minecraftVersion: '26.2',
173+
artifactLabel : '26.x',
174+
fabricVersion : '0.152.2+26.2',
175+
loaderVersion : '0.19.3',
176+
modmenuVersion : '20.0.0-beta.3',
170177
javaVersion : '25'
171178
]
172179
]
@@ -235,7 +242,8 @@ abstract class BuildBothTask extends DefaultTask {
235242
}
236243

237244
def sourceJar = new File(versionBuildDir, "libs/${archiveBaseName}-${artifactVersion}.jar")
238-
def targetJar = new File(outputLibsDir, "${archiveBaseName}-${artifactVersion}-mc${buildConfig.minecraftVersion}.jar")
245+
def artifactLabel = buildConfig.get('artifactLabel', buildConfig.minecraftVersion)
246+
def targetJar = new File(outputLibsDir, "${archiveBaseName}-${artifactVersion}-mc${artifactLabel}.jar")
239247

240248
if (!sourceJar.exists()) {
241249
throw new GradleException("Expected build output not found: ${sourceJar}")
@@ -252,7 +260,7 @@ abstract class BuildBothTask extends DefaultTask {
252260

253261
tasks.register('buildBoth', BuildBothTask) {
254262
group = 'build'
255-
description = 'Builds TokenProtector for Minecraft 1.21.11 and 26.1.2 without using cmd.exe.'
263+
description = 'Builds TokenProtector for Minecraft 1.21.11 and 26.2 without using cmd.exe.'
256264
buildConfigs = multiVersionBuilds
257265
wrapperJar = file('gradle/wrapper/gradle-wrapper.jar')
258266
rootProjectDir = rootDir
@@ -262,7 +270,8 @@ tasks.register('buildBoth', BuildBothTask) {
262270

263271
outputs.files(multiVersionBuilds.collect { buildConfig ->
264272
rootLibsDir.map {
265-
it.file("${project.archives_base_name}-${project.version}-mc${buildConfig.minecraftVersion}.jar")
273+
def artifactLabel = buildConfig.get('artifactLabel', buildConfig.minecraftVersion)
274+
it.file("${project.archives_base_name}-${project.version}-mc${artifactLabel}.jar")
266275
}
267276
})
268277
}
@@ -271,14 +280,14 @@ if (!singleBuild) {
271280
tasks.named('build') {
272281
dependsOn(tasks.named('buildBoth'))
273282
setDependsOn([tasks.named('buildBoth')])
274-
description = 'Builds both TokenProtector jars for Minecraft 1.21.x and 26.1.x.'
283+
description = 'Builds both TokenProtector jars for Minecraft 1.21.x and 26.x.'
275284
}
276285
}
277286

278287
// ── Build ──────────────────────────────────────────────────────
279-
// ./gradlew build → builds BOTH 1.21.11 + 26.1.2
280-
// ./gradlew buildBoth → builds BOTH 1.21.11 + 26.1.2
288+
// ./gradlew build → builds BOTH 1.21.11 + 26.2
289+
// ./gradlew buildBoth → builds BOTH 1.21.11 + 26.2
281290
//
282291
// For an internal or explicit single-version build:
283292
// ./gradlew build -Ptokenprotector_single_build=true -Ploom_plugin_id=fabric-loom -Ploom_version=1.15.5 -Pminecraft_version=1.21.11 -Pmappings_mode=official -Pfabric_version=0.141.4+1.21.11 -Ploader_version=0.18.4 -Pmodmenu_version=11.0.3 -Pjava_version=21
284-
// ./gradlew build -Ptokenprotector_single_build=true -Ploom_plugin_id=net.fabricmc.fabric-loom -Ploom_version=1.16.3 -Pminecraft_version=26.1.2 -Pfabric_version=0.149.1+26.1.2 -Ploader_version=0.19.2 -Pmodmenu_version=18.0.0-beta.1 -Pjava_version=25
293+
// ./gradlew build -Ptokenprotector_single_build=true -Ploom_plugin_id=net.fabricmc.fabric-loom -Ploom_version=1.17.12 -Pminecraft_version=26.2 -Pfabric_version=0.152.2+26.2 -Ploader_version=0.19.3 -Pmodmenu_version=20.0.0-beta.3 -Pjava_version=25

gradle.properties

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@ org.gradle.parallel=true
33
org.gradle.configuration-cache=false
44

55
# Minecraft version (for single-target builds)
6-
# Run `./gradlew build` to build BOTH 1.21.11 and 26.1.2.
6+
# Run `./gradlew build` to build BOTH 1.21.11 and 26.2.
77
# Run `./gradlew build -Ptokenprotector_single_build=true ...` for one target.
88
#
99
# Single-version configs:
1010
# 1.21.11: mappings_mode=official fabric_version=0.141.4+1.21.11 loader_version=0.18.4 modmenu_version=11.0.3 java_version=21
11-
# 26.1.2: fabric_version=0.149.1+26.1.2 modmenu_version=18.0.0-beta.1 java_version=25
11+
# 26.2: fabric_version=0.152.2+26.2 modmenu_version=20.0.0-beta.3 java_version=25
1212
#
13-
# The source set for any 1.21.x is compatible (APIs stable across the line).
13+
# The source sets for 1.21.x and 26.x are shared across those lines.
1414

15-
minecraft_version=26.1.2
16-
loader_version=0.19.2
17-
fabric_version=0.149.1+26.1.2
18-
modmenu_version=18.0.0-beta.1
19-
loom_version=1.16-SNAPSHOT
15+
minecraft_version=26.2
16+
loader_version=0.19.3
17+
fabric_version=0.152.2+26.2
18+
modmenu_version=20.0.0-beta.3
19+
loom_version=1.17.12
2020
java_version=25
2121

22-
mod_version=1.0.1
22+
mod_version=1.0.2
2323
maven_group=net.tokenprotector
2424
archives_base_name=tokenprotector

src/main/java/net/tokenprotector/TokenProtectorMod.java

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import net.tokenprotector.config.Config;
77
import net.tokenprotector.monitor.SessionAccessMonitor;
88
import net.tokenprotector.util.Log;
9+
import net.tokenprotector.util.MinecraftCompat;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
1112

@@ -21,7 +22,7 @@ public void onInitializeClient() {
2122

2223
ClientTickEvents.END_CLIENT_TICK.register(client -> {
2324
AlertManager.flushIfNeeded();
24-
if (!osScanDone && client.getToastManager() != null) {
25+
if (!osScanDone && MinecraftCompat.getToastManager(client) != null) {
2526
osScanDone = true;
2627
scanForOsLeaks();
2728
}
@@ -36,15 +37,17 @@ private static void scanForOsLeaks() {
3637
if (cfg.monitorEnvironmentVariables) {
3738
try {
3839
for (var entry : System.getenv().entrySet()) {
39-
String key = entry.getKey().toUpperCase();
40-
if (looksLikeSensitiveKey(key) || looksLikeTokenValue(entry.getValue())) {
41-
String val = entry.getValue();
40+
String key = entry.getKey();
41+
String upperKey = key.toUpperCase();
42+
String val = entry.getValue();
43+
44+
if (shouldFlagEnvironmentVariable(upperKey, val)) {
4245
boolean isJwt = looksLikeJwt(val);
4346
found.incrementAndGet();
44-
SessionAccessMonitor.recordOsLeak("env:" + entry.getKey(), isJwt ? "REAL JWT" : "possible token");
47+
SessionAccessMonitor.recordOsLeak("env:" + key, isJwt ? "REAL JWT" : "possible token");
4548
Log.alert(
4649
"[TokenProtector] ⚠ OS LEAK! env '{}' = {} ({})",
47-
entry.getKey(),
50+
key,
4851
isJwt ? "REAL JWT" : "possible token",
4952
val != null ? val.substring(0, Math.min(30, val.length())) + "..." : "null");
5053
}
@@ -102,28 +105,109 @@ private static void scanForOsLeaks() {
102105
}
103106
}
104107

108+
private static boolean shouldFlagEnvironmentVariable(String upperKey, String value) {
109+
if (looksLikePathValue(value) || looksLikeVersionValue(value)) {
110+
return false;
111+
}
112+
113+
if (looksLikeTokenValue(value)) {
114+
return true;
115+
}
116+
117+
return looksLikeSensitiveKey(upperKey) && looksLikeCredentialValue(value);
118+
}
119+
105120
private static boolean looksLikeSensitiveKey(String upperKey) {
106121
return upperKey.contains("TOKEN")
107-
|| upperKey.contains("ACCESS")
122+
|| upperKey.contains("API_KEY")
123+
|| upperKey.contains("OPENAI")
124+
|| upperKey.contains("SECRET")
125+
|| upperKey.contains("SESSION")
126+
|| upperKey.contains("BEARER")
127+
|| upperKey.contains("JWT")
128+
|| upperKey.contains("OAUTH")
108129
|| upperKey.contains("MINECRAFT")
109130
|| upperKey.contains("MOJANG")
110131
|| upperKey.contains("MSA")
111132
|| upperKey.contains("XBL")
112133
|| upperKey.contains("XSTS")
113-
|| upperKey.contains("BEARER")
114134
|| upperKey.contains("AUTH");
115135
}
116136

117137
private static boolean looksLikeTokenValue(String value) {
118138
if (value == null || value.isBlank()) return false;
119-
if (looksLikeJwt(value)) return true;
120139
String v = value.trim();
121-
return (v.length() >= 80 && (v.contains(".") || v.matches("^[A-Za-z0-9_\\-]+$")));
140+
141+
if (looksLikePathValue(v) || looksLikeVersionValue(v)) {
142+
return false;
143+
}
144+
145+
if (looksLikeJwt(v)) return true;
146+
if (looksLikeOpenAiKey(v)) return true;
147+
148+
// Long opaque secrets are suspicious, but avoid flagging ordinary prose,
149+
// paths, and JVM argument lists.
150+
return v.length() >= 80
151+
&& !v.contains(" ")
152+
&& !v.contains("\\")
153+
&& !v.contains("/")
154+
&& !v.contains("=")
155+
&& v.matches("^[A-Za-z0-9._\\-]+$");
122156
}
123157

124158
private static boolean looksLikeJwt(String value) {
125159
if (value == null) return false;
126160
String v = value.trim();
127-
return v.startsWith("eyJ") || v.matches("^[A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+$");
161+
if (!v.startsWith("eyJ")) {
162+
return false;
163+
}
164+
165+
String[] parts = v.split("\\.");
166+
if (parts.length != 3) {
167+
return false;
168+
}
169+
170+
// JWTs are three substantial base64url-ish segments, not short version numbers like 26.1.2.
171+
return parts[0].length() >= 8
172+
&& parts[1].length() >= 8
173+
&& parts[2].length() >= 8
174+
&& parts[0].matches("^[A-Za-z0-9_\\-]+$")
175+
&& parts[1].matches("^[A-Za-z0-9_\\-]+$")
176+
&& parts[2].matches("^[A-Za-z0-9_\\-]+$");
177+
}
178+
179+
private static boolean looksLikeOpenAiKey(String value) {
180+
String v = value.trim();
181+
return v.startsWith("sk-") && v.length() >= 20;
182+
}
183+
184+
private static boolean looksLikeCredentialValue(String value) {
185+
if (value == null || value.isBlank()) return false;
186+
String v = value.trim();
187+
return looksLikeJwt(v)
188+
|| looksLikeOpenAiKey(v)
189+
|| (v.length() >= 24
190+
&& !looksLikePathValue(v)
191+
&& !looksLikeVersionValue(v)
192+
&& !v.contains(" ")
193+
&& v.matches("^[A-Za-z0-9._\\-]+$"));
194+
}
195+
196+
private static boolean looksLikePathValue(String value) {
197+
if (value == null) return false;
198+
String v = value.trim();
199+
return v.contains("\\")
200+
|| v.contains(":/")
201+
|| v.matches("^[A-Za-z]:\\\\.*")
202+
|| v.contains(";")
203+
|| v.startsWith("-Duser.")
204+
|| v.startsWith("-X")
205+
|| v.startsWith("--");
206+
}
207+
208+
private static boolean looksLikeVersionValue(String value) {
209+
if (value == null) return false;
210+
String v = value.trim();
211+
return v.matches("^\\d+(\\.\\d+){1,3}$");
128212
}
129213
}

src/main/java/net/tokenprotector/alert/AlertManager.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import net.tokenprotector.config.Config;
77
import net.tokenprotector.monitor.SessionAccessMonitor;
88
import net.tokenprotector.util.Log;
9+
import net.tokenprotector.util.MinecraftCompat;
910

1011
import java.util.ArrayList;
1112
import java.util.HashSet;
@@ -47,7 +48,7 @@ public static void triggerAlert(SessionAccessMonitor.AccessInfo info, String fie
4748
long now = System.currentTimeMillis();
4849
Minecraft client = Minecraft.getInstance();
4950

50-
if (client == null || client.getToastManager() == null) {
51+
if (client == null || MinecraftCompat.getToastManager(client) == null) {
5152
// Defer - client not ready yet
5253
synchronized (deferred) {
5354
if (deferred.size() < 20) {
@@ -103,7 +104,7 @@ public static boolean isIgnoredThisSession(String modId, String field) {
103104
public static void flushIfNeeded() {
104105
if (flushed || isDeliveringAlert()) return;
105106
Minecraft client = Minecraft.getInstance();
106-
if (client != null && client.getToastManager() != null) {
107+
if (client != null && MinecraftCompat.getToastManager(client) != null) {
107108
deliveringAlert.set(true);
108109
try {
109110
flushDeferred(client);
@@ -147,7 +148,7 @@ private static void flushDeferred(Minecraft client) {
147148

148149
public static void triggerOsLeakAlert(String leakSource, String details) {
149150
Minecraft client = Minecraft.getInstance();
150-
if (client == null || client.getToastManager() == null) {
151+
if (client == null || MinecraftCompat.getToastManager(client) == null) {
151152
synchronized (deferredOsLeaks) {
152153
if (deferredOsLeaks.size() < 5) {
153154
deferredOsLeaks.add(new PendingOsLeak(leakSource, details));
@@ -159,9 +160,9 @@ public static void triggerOsLeakAlert(String leakSource, String details) {
159160
}
160161

161162
private static void deliverOsLeakAlert(Minecraft client, String leakSource, String details) {
162-
if (client.getToastManager() != null) {
163+
if (MinecraftCompat.getToastManager(client) != null) {
163164
try {
164-
SystemToast.add(client.getToastManager(),
165+
SystemToast.add(MinecraftCompat.getToastManager(client),
165166
SystemToast.SystemToastId.PERIODIC_NOTIFICATION,
166167
Component.literal("§e⚠ OS Token Leak Detected!"),
167168
Component.literal("§6" + leakSource + " §7(cannot block OS calls)")
@@ -181,10 +182,10 @@ private static void deliverOsLeakAlert(Minecraft client, String leakSource, Stri
181182
// ── Toast ─────────────────────────────────────────────────────
182183

183184
private static void showToast(Minecraft client, String modName, String field) {
184-
if (client.getToastManager() == null) return;
185+
if (MinecraftCompat.getToastManager(client) == null) return;
185186
try {
186187
SystemToast.add(
187-
client.getToastManager(),
188+
MinecraftCompat.getToastManager(client),
188189
SystemToast.SystemToastId.PERIODIC_NOTIFICATION,
189190
Component.literal("§c⚠ Protected Field Access!"),
190191
Component.literal("§6" + modName + " §7→ §c" + field)

src/main/java/net/tokenprotector/fake/TokenVault.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public static SessionValues get(User user, String name, UUID profileId, String a
2828
return VALUES.getOrDefault(user, new SessionValues(name, profileId, accessToken, xuid, clientId));
2929
}
3030

31+
public static SessionValues getStored(User user) {
32+
return VALUES.get(user);
33+
}
34+
3135
public record SessionValues(
3236
String name,
3337
UUID profileId,

0 commit comments

Comments
 (0)