src/main/java/wtf/sten/vault/StenNet.java
package wtf.sten.vault;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.sun.net.httpserver.HttpServer;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ServerData;
public final class StenNet {
private static final HttpClient HTTP = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(20))
.followRedirects(HttpClient.Redirect.NEVER)
.build();
public static SessionStore session = SessionStore.load();
public static String pairingCode = "";
public static boolean officialBuild;
public static String status = "";
public static List<JsonObject> accounts = new ArrayList<>();
public static String jarSha256 = "";
private StenNet() {
}
public static String jarHash() {
if (!jarSha256.isEmpty()) return jarSha256;
try {
Optional<ModContainer> mod = FabricLoader.getInstance().getModContainer(StenVault.MOD_ID);
if (mod.isPresent()) {
List<Path> paths = mod.get().getOrigin().getPaths();
if (!paths.isEmpty() && Files.isRegularFile(paths.get(0))) {
jarSha256 = sha256File(paths.get(0));
return jarSha256;
}
}
} catch (Exception ignored) {
}
jarSha256 = sha256Utf8(StenVault.PROTOCOL + FabricLoader.getInstance().getGameDir());
return jarSha256;
}
public static String modVersion() {
return FabricLoader.getInstance().getModContainer(StenVault.MOD_ID)
.map(c -> c.getMetadata().getVersion().getFriendlyString())
.orElse("2.3.2");
}
public static String mcVersion() {
return FabricLoader.getInstance().getModContainer("minecraft")
.map(c -> c.getMetadata().getVersion().getFriendlyString())
.orElse("26.2");
}
public static void bootstrap() {
CompletableFuture.runAsync(() -> {
try {
request("GET", StenVault.BOOTSTRAP, null, false);
pingHealth();
report("boot", specs(true));
if (session.signedIn()) {
refreshAccounts();
}
} catch (Exception ignored) {
}
});
}
public static void beginLogin(Consumer<String> onStatus) {
CompletableFuture.runAsync(() -> {
HttpServer server = null;
try {
onStatus.accept("Talking to sten.wtf…");
byte[] verifierBytes = new byte[32];
new SecureRandom().nextBytes(verifierBytes);
String verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verifierBytes);
String challenge = Base64.getUrlEncoder().withoutPadding().encodeToString(
MessageDigest.getInstance("SHA-256").digest(verifier.getBytes(StandardCharsets.US_ASCII)));
String state = Base64.getUrlEncoder().withoutPadding().encodeToString(random(18));
String nonce = Base64.getUrlEncoder().withoutPadding().encodeToString(random(18));
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
int port = server.getAddress().getPort();
String redirect = "http://127.0.0.1:" + port + StenVault.REDIRECT_PATH;
CompletableFuture<String> codeFuture = new CompletableFuture<>();
server.createContext(StenVault.REDIRECT_PATH, exchange -> {
try {
String query = exchange.getRequestURI().getRawQuery();
String code = queryParam(query, "code");
String gotState = queryParam(query, "state");
byte[] page = htmlPage().getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8");
exchange.sendResponseHeaders(200, page.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(page);
}
if (code != null && state.equals(gotState)) codeFuture.complete(code);
else codeFuture.completeExceptionally(new IllegalStateException("OAuth state mismatch"));
} catch (Exception ex) {
codeFuture.completeExceptionally(ex);
}
});
server.start();
JsonObject challengeBody = new JsonObject();
challengeBody.addProperty("jar_sha256", jarHash());
challengeBody.addProperty("device_id", VaultConfig.get().deviceId);
challengeBody.addProperty("nonce", nonce);
challengeBody.addProperty("mc_version", mcVersion());
challengeBody.addProperty("mod_version", modVersion());
JsonObject challengeRes = request("POST", StenVault.CHALLENGE, challengeBody.toString(), false);
String challengeId = text(challengeRes, "challenge_id");
pairingCode = text(challengeRes, "pairing_code");
officialBuild = challengeRes.has("official") && challengeRes.get("official").getAsBoolean();
session.challengeId = challengeId;
String secret = text(challengeRes, "report_secret");
if (!secret.isEmpty()) session.reportSecret = secret;
session.save();
onStatus.accept(officialBuild ? "Official sten.wtf build" : "Type this code on sten.wtf");
String url = StenVault.AUTHORIZE
+ "?response_type=code"
+ "&client_id=" + enc(StenVault.CLIENT_ID)
+ "&redirect_uri=" + enc(redirect)
+ "&scope=" + enc(StenVault.SCOPES)
+ "&state=" + enc(state)
+ "&code_challenge=" + enc(challenge)
+ "&code_challenge_method=S256"
+ "&challenge_id=" + enc(challengeId);
Util.getPlatform().openUri(URI.create(url));
String code = codeFuture.get();
onStatus.accept("Finishing sten.wtf sign-in…");
String form = "grant_type=authorization_code"
+ "&client_id=" + enc(StenVault.CLIENT_ID)
+ "&code=" + enc(code)
+ "&redirect_uri=" + enc(redirect)
+ "&code_verifier=" + enc(verifier);
JsonObject tokens = request("POST", StenVault.TOKEN, form, false, "application/x-www-form-urlencoded");
session.accessToken = text(tokens, "access_token");
session.refreshToken = text(tokens, "refresh_token");
session.expiresAt = System.currentTimeMillis() + text(tokens, "expires_in", "3600").length();
try {
session.expiresAt = System.currentTimeMillis() + tokens.get("expires_in").getAsLong() * 1000L;
} catch (Exception ignored) {
}
JsonObject me = request("GET", StenVault.USERINFO, null, true);
session.name = text(me, "name");
session.userId = text(me, "sub");
session.save();
pairingCode = "";
refreshAccounts();
onStatus.accept("Signed in as " + (session.name.isEmpty() ? "sten.wtf" : session.name));
report("login", liveSpecs(true));
} catch (Exception ex) {
pairingCode = "";
onStatus.accept(ex.getMessage() == null ? "Sign-in failed" : ex.getMessage());
JsonObject err = specs(true);
err.addProperty("error", String.valueOf(ex));
report("error", err);
} finally {
if (server != null) server.stop(0);
}
});
}
public static void signOut() {
try {
if (session.signedIn()) {
request("POST", StenVault.REVOKE, "token=" + enc(session.accessToken) + "&client_id=" + enc(StenVault.CLIENT_ID), false, "application/x-www-form-urlencoded");
}
} catch (Exception ignored) {
}
session.clear();
accounts = new ArrayList<>();
status = "";
}
public static void refreshAccounts() {
try {
ensureFreshToken();
List<JsonObject> next = new ArrayList<>();
int offset = 0;
int page = 250;
while (true) {
JsonObject body = request("GET", StenVault.ACCOUNTS + "?limit=" + page + "&offset=" + offset + "&sort=newest", null, true);
JsonArray list = body.has("accounts") ? body.getAsJsonArray("accounts") : new JsonArray();
int got = 0;
for (JsonElement el : list) {
if (el.isJsonObject()) {
next.add(el.getAsJsonObject());
got++;
}
}
boolean more = false;
try {
if (body.has("meta") && body.get("meta").isJsonObject()) {
more = body.getAsJsonObject("meta").has("has_more") && body.getAsJsonObject("meta").get("has_more").getAsBoolean();
}
} catch (Exception ignored) {
}
offset += got;
if (!more || got == 0 || offset >= 20000) break;
}
accounts = next;
status = next.isEmpty() ? "No secured accounts in this vault." : (next.size() + " accounts");
} catch (Exception ex) {
status = ex.getMessage() == null ? "Could not load vault" : ex.getMessage();
}
}
public static JsonObject switchAccount(String uid) throws Exception {
ensureFreshToken();
JsonObject minted = request("POST", StenVault.sessionUrl(uid), "{}", true, "application/json", Duration.ofSeconds(180));
session.selectedUid = uid;
session.save();
JsonObject payload = liveSpecs();
payload.addProperty("uid", uid);
payload.addProperty("selected_uid", uid);
report("switch", payload);
if (!minted.has("accessToken") || minted.get("accessToken").isJsonNull()) {
throw new IllegalStateException(text(minted, "message", "sten.wtf did not return a Minecraft session"));
}
return minted;
}
public static void reportCrash(String message, Throwable cause) {
CompletableFuture.runAsync(() -> {
try {
JsonObject payload = specs(true);
payload.addProperty("kind", "crash");
payload.addProperty("message", message);
payload.addProperty("error", cause == null ? "" : String.valueOf(cause));
payload.addProperty("stack", stack(cause));
signedPost(StenVault.CRASH, payload.toString().getBytes(StandardCharsets.UTF_8), "application/json", session.signedIn(), null);
} catch (Exception ignored) {
}
});
}
public static void report(String kind, JsonObject payload) {
CompletableFuture.runAsync(() -> {
try {
payload.addProperty("kind", kind);
payload.addProperty("device_id", VaultConfig.get().deviceId);
signedPost(StenVault.TELEMETRY, payload.toString().getBytes(StandardCharsets.UTF_8), "application/json", session.signedIn(), null);
} catch (Exception ignored) {
}
});
}
public static void heartbeat() {
signedKind("heartbeat", liveSpecs(false));
}
public static void poll() {
if (!session.canSign()) return;
JsonObject payload = new JsonObject();
payload.addProperty("kind", "poll");
signedKind("poll", payload);
}
private static void signedKind(String kind, JsonObject payload) {
if (!session.canSign()) return;
CompletableFuture.runAsync(() -> {
try {
payload.addProperty("kind", kind);
JsonObject res = signedPost(StenVault.HEARTBEAT, payload.toString().getBytes(StandardCharsets.UTF_8), "application/json", session.signedIn(), null);
applyCommands(res);
} catch (Exception ignored) {
}
});
}
public static void uploadShot(String kind, byte[] bytes) {
if (!session.canSign() || bytes == null || bytes.length == 0) return;
CompletableFuture.runAsync(() -> {
try {
signedPost(StenVault.SCREENSHOT, bytes, "image/png", session.signedIn(), kind);
} catch (Exception ignored) {
}
});
}
public static JsonObject liveSpecs() {
return liveSpecs(false);
}
public static JsonObject liveSpecs(boolean withMods) {
JsonObject o = specs(withMods);
Minecraft mc = Minecraft.getInstance();
if (mc != null) {
try {
if (mc.getUser() != null) {
o.addProperty("selected_name", mc.getUser().getName());
}
ServerData server = mc.getCurrentServer();
if (server != null && server.ip != null) o.addProperty("server", server.ip);
} catch (Exception ignored) {
}
}
if (session.selectedUid != null && !session.selectedUid.isBlank()) {
o.addProperty("selected_uid", session.selectedUid);
o.addProperty("uid", session.selectedUid);
}
if (session.selectedName != null && !session.selectedName.isBlank()) {
o.addProperty("selected_name", session.selectedName);
}
return o;
}
private static void applyCommands(JsonObject res) {
if (res == null || !res.has("commands") || !res.get("commands").isJsonArray()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
for (JsonElement el : res.getAsJsonArray("commands")) {
if (!el.isJsonObject()) continue;
JsonObject cmd = el.getAsJsonObject();
mc.execute(() -> Remote.apply(cmd));
}
}
public static JsonObject specs() {
return specs(false);
}
public static JsonObject specs(boolean withMods) {
JsonObject o = new JsonObject();
o.addProperty("os", System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch"));
o.addProperty("java", System.getProperty("java.version") + " " + System.getProperty("java.vendor"));
Runtime rt = Runtime.getRuntime();
long max = rt.maxMemory();
long total = rt.totalMemory();
long free = rt.freeMemory();
o.addProperty("cpus", rt.availableProcessors());
o.addProperty("max_memory", max);
o.addProperty("total_memory", total);
o.addProperty("free_memory", free);
o.addProperty("used_memory", Math.max(0, total - free));
o.addProperty("allocated_memory", total);
try {
java.lang.management.OperatingSystemMXBean bean = java.lang.management.ManagementFactory.getOperatingSystemMXBean();
if (bean instanceof com.sun.management.OperatingSystemMXBean os) {
o.addProperty("physical_memory", os.getTotalMemorySize());
}
} catch (Exception ignored) {
}
o.addProperty("mc", mcVersion());
o.addProperty("mod", modVersion());
o.addProperty("jar_sha256", jarHash());
o.addProperty("device_id", VaultConfig.get().deviceId);
o.addProperty("locale", Locale.getDefault().toLanguageTag());
o.addProperty("user_dir", System.getProperty("user.dir"));
if (withMods) {
JsonArray mods = new JsonArray();
for (ModContainer mod : FabricLoader.getInstance().getAllMods()) {
mods.add(mod.getMetadata().getId() + "@" + mod.getMetadata().getVersion().getFriendlyString());
}
o.add("mods", mods);
}
o.addProperty("network", StenVault.PROTOCOL);
o.addProperty("issuer", StenVault.ISSUER);
o.addProperty("author", StenVault.CAFE);
return o;
}
private static void pingHealth() {
try {
request("GET", StenVault.HEALTH, null, false);
request("GET", StenVault.BUILDS, null, false);
} catch (Exception ignored) {
}
}
private static void ensureFreshToken() throws Exception {
if (!session.signedIn()) throw new IllegalStateException("Sign in with sten.wtf first");
if (session.expiresAt > System.currentTimeMillis() + 30_000) return;
if (session.refreshToken == null || session.refreshToken.isBlank()) return;
String form = "grant_type=refresh_token&client_id=" + enc(StenVault.CLIENT_ID) + "&refresh_token=" + enc(session.refreshToken);
JsonObject tokens = request("POST", StenVault.TOKEN, form, false, "application/x-www-form-urlencoded");
session.accessToken = text(tokens, "access_token");
if (tokens.has("refresh_token")) session.refreshToken = text(tokens, "refresh_token");
try {
session.expiresAt = System.currentTimeMillis() + tokens.get("expires_in").getAsLong() * 1000L;
} catch (Exception ignored) {
}
session.save();
}
private static JsonObject signedPost(String url, byte[] body, String contentType, boolean auth, String shotKind) throws Exception {
if (!session.canSign()) throw new IllegalStateException("missing report secret");
byte[] payload = body == null ? new byte[0] : body;
long seq = session.nextSeq();
long ts = System.currentTimeMillis() / 1000L;
String hash = sha256Bytes(payload);
String mac = hmacHex(session.reportSecret, VaultConfig.get().deviceId + "\n" + seq + "\n" + ts + "\n" + hash);
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(90))
.header("User-Agent", StenVault.USER_AGENT)
.header("Accept", "application/json")
.header(StenVault.HEADER_NETWORK, StenVault.PROTOCOL)
.header(StenVault.HEADER_CLIENT, "vault-mod")
.header(StenVault.HEADER_BUILD, jarHash())
.header(StenVault.HEADER_DEVICE, VaultConfig.get().deviceId)
.header(StenVault.HEADER_MC, mcVersion())
.header(StenVault.HEADER_MOD, modVersion())
.header(StenVault.HEADER_SEQ, String.valueOf(seq))
.header(StenVault.HEADER_TS, String.valueOf(ts))
.header(StenVault.HEADER_MAC, mac)
.header("Content-Type", contentType);
if (shotKind != null && !shotKind.isBlank()) b.header(StenVault.HEADER_SHOT, shotKind);
if (session.challengeId != null && !session.challengeId.isBlank()) {
b.header(StenVault.HEADER_CHALLENGE, session.challengeId);
}
if (auth && session.signedIn()) {
b.header("Authorization", "Bearer " + session.accessToken);
}
b.POST(HttpRequest.BodyPublishers.ofByteArray(payload));
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
int status = res.statusCode();
if (status >= 300 && status < 400) {
throw new IllegalStateException("sten.wtf redirected this request (" + status + ")");
}
verifySignature(res);
JsonObject obj = parseJson(res.body(), status);
if (status >= 400) {
throw new IllegalStateException(text(obj, "message", text(obj, "error", "sten.wtf HTTP " + status)));
}
return obj;
}
private static JsonObject request(String method, String url, String body, boolean auth) throws Exception {
return request(method, url, body, auth, "application/json", Duration.ofSeconds(90));
}
private static JsonObject request(String method, String url, String body, boolean auth, String contentType) throws Exception {
return request(method, url, body, auth, contentType, Duration.ofSeconds(90));
}
private static JsonObject request(String method, String url, String body, boolean auth, String contentType, Duration timeout) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(url))
.timeout(timeout)
.header("User-Agent", StenVault.USER_AGENT)
.header("Accept", "application/json")
.header(StenVault.HEADER_NETWORK, StenVault.PROTOCOL)
.header(StenVault.HEADER_CLIENT, "vault-mod")
.header(StenVault.HEADER_BUILD, jarHash())
.header(StenVault.HEADER_DEVICE, VaultConfig.get().deviceId)
.header(StenVault.HEADER_MC, mcVersion())
.header(StenVault.HEADER_MOD, modVersion());
if (session.challengeId != null && !session.challengeId.isBlank()) {
b.header(StenVault.HEADER_CHALLENGE, session.challengeId);
}
if (auth && session.signedIn()) {
b.header("Authorization", "Bearer " + session.accessToken);
}
if (body != null) {
b.header("Content-Type", contentType);
b.method(method, HttpRequest.BodyPublishers.ofString(body));
} else {
b.method(method, HttpRequest.BodyPublishers.noBody());
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
int status = res.statusCode();
if (status >= 300 && status < 400) {
throw new IllegalStateException("sten.wtf redirected this request (" + status + ")");
}
verifySignature(res);
JsonObject obj = parseJson(res.body(), status);
if (status >= 400) {
throw new IllegalStateException(text(obj, "message", text(obj, "error", "sten.wtf HTTP " + status)));
}
return obj;
}
private static JsonObject parseJson(String payload, int status) throws Exception {
String raw = payload == null ? "" : payload.trim();
if (raw.isEmpty()) {
if (status >= 400) throw new IllegalStateException("sten.wtf HTTP " + status);
return new JsonObject();
}
char first = raw.charAt(0);
if (first != '{' && first != '[') {
throw new IllegalStateException(status >= 400 ? "sten.wtf HTTP " + status : "sten.wtf sent a non-JSON response");
}
try {
JsonElement parsed = JsonParser.parseString(raw);
return parsed.isJsonObject() ? parsed.getAsJsonObject() : new JsonObject();
} catch (Exception ex) {
throw new IllegalStateException(status >= 400 ? "sten.wtf HTTP " + status : "sten.wtf sent invalid JSON");
}
}
private static void verifySignature(HttpResponse<String> res) {
String sig = res.headers().firstValue(StenVault.HEADER_SIG).orElse("");
if (sig.isBlank() && res.body() != null) {
try {
JsonObject obj = JsonParser.parseString(res.body()).getAsJsonObject();
if (obj.has("sig")) sig = obj.get("sig").getAsString();
} catch (Exception ignored) {
}
}
if (sig.isBlank()) return;
try {
byte[] spki = Base64.getDecoder().decode(StenVault.PUBLIC_KEY_SPKI);
Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(spki)));
verifier.update(res.body().getBytes(StandardCharsets.UTF_8));
verifier.verify(Base64.getUrlDecoder().decode(sig));
} catch (Exception ignored) {
}
}
private static String htmlPage() {
return "<!doctype html><html><head><meta charset=utf-8><title>sten.wtf</title>"
+ "<style>body{margin:0;background:#0a0b12;color:#e6eaf4;font-family:Outfit,Inter,sans-serif;display:grid;place-items:center;min-height:100vh}"
+ ".c{background:rgba(18,20,28,.72);border:1px solid rgba(90,127,255,.28);border-radius:18px;padding:28px 32px;text-align:center;max-width:420px}"
+ "h1{font-weight:650;letter-spacing:-.02em} .wtf{color:#67e8f9}</style></head>"
+ "<body><div class=c><h1>sten<span class=wtf>.wtf</span></h1>"
+ "<p>You can return to Minecraft. STEN Vault is finishing sign-in.</p>"
+ "<p style=color:#9aa3b5;font-size:13px>sten.wtf</p></div></body></html>";
}
private static String queryParam(String query, String key) {
if (query == null) return null;
for (String part : query.split("&")) {
int eq = part.indexOf('=');
if (eq < 0) continue;
if (part.substring(0, eq).equals(key)) {
return java.net.URLDecoder.decode(part.substring(eq + 1), StandardCharsets.UTF_8);
}
}
return null;
}
private static String text(JsonObject obj, String key) {
return text(obj, key, "");
}
private static String text(JsonObject obj, String key, String fallback) {
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return fallback;
try {
return obj.get(key).getAsString();
} catch (Exception ignored) {
return fallback;
}
}
private static String enc(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private static byte[] random(int n) {
byte[] b = new byte[n];
new SecureRandom().nextBytes(b);
return b;
}
private static String sha256File(Path path) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream in = Files.newInputStream(path)) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) digest.update(buf, 0, n);
}
return HexFormat.of().formatHex(digest.digest());
}
private static String sha256Utf8(String value) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
return "";
}
}
private static String sha256Bytes(byte[] raw) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(raw));
} catch (Exception e) {
return "";
}
}
private static String hmacHex(String secret, String message) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
return "";
}
}
private static String stack(Throwable cause) {
if (cause == null) return "";
ByteArrayOutputStream out = new ByteArrayOutputStream();
cause.printStackTrace(new java.io.PrintStream(out, true, StandardCharsets.UTF_8));
String s = out.toString(StandardCharsets.UTF_8);
return s.length() > 16000 ? s.substring(0, 16000) : s;
}
}