aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker
diff options
context:
space:
mode:
authorAaron <51387595+AzureAaron@users.noreply.github.com>2024-06-16 11:34:58 -0400
committerAaron <51387595+AzureAaron@users.noreply.github.com>2024-06-18 16:54:00 -0400
commit163905a63d840a4dd17b29bb53c6e7be0bf55c03 (patch)
tree378aec8383f378b27df68f9b2e94bb51a5e3cc8d /src/main/java/de/hysky/skyblocker
parentd6b220a8e42a1fc2dbc955779e86d199851b4674 (diff)
downloadSkyblocker-163905a63d840a4dd17b29bb53c6e7be0bf55c03.tar.gz
Skyblocker-163905a63d840a4dd17b29bb53c6e7be0bf55c03.tar.bz2
Skyblocker-163905a63d840a4dd17b29bb53c6e7be0bf55c03.zip
API Authentication
Diffstat (limited to 'src/main/java/de/hysky/skyblocker')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java149
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Http.java36
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java4
4 files changed, 179 insertions, 12 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index eff88783..21790256 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -174,8 +174,8 @@ public class SkyblockerMod implements ClientModInitializer {
ItemRarityBackgrounds.init();
MuseumItemCache.init();
SecretsTracker.init();
+ ApiAuthentication.init();
ApiUtils.init();
- ProfileUtils.init();
Debug.init();
Kuudra.init();
RenderHelper.init();
diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
new file mode 100644
index 00000000..16d07ac5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
@@ -0,0 +1,149 @@
+package de.hysky.skyblocker.utils;
+
+import java.nio.ByteBuffer;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.UUID;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import com.google.gson.JsonParser;
+import com.mojang.logging.LogUtils;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.SharedConstants;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.network.encryption.PlayerKeyPair;
+import net.minecraft.text.Text;
+import net.minecraft.util.Uuids;
+import net.minecraft.util.dynamic.Codecs;
+
+public class ApiAuthentication {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final String MINECRAFT_VERSION = SharedConstants.getGameVersion().getName();
+ private static final String AUTH_URL = "https://api.azureaaron.net/authenticate";
+ private static final String CONTENT_TYPE = "application/json";
+ private static final String ALGORITHM = "SHA256withRSA";
+
+ private static TokenInfo tokenInfo = null;
+
+ public static void init() {
+ //Update token after the profileKeys instance is initialized
+ ClientLifecycleEvents.CLIENT_STARTED.register(_client -> updateToken());
+ }
+
+ private static void updateToken() {
+ //The fetching runs async in ProfileKeysImpl#getKeyPair
+ CLIENT.getProfileKeys().fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> {
+ if (playerKeypairOpt.isPresent()) {
+ PlayerKeyPair playerKeyPair = playerKeypairOpt.get();
+
+ //The key header and footer can be sent but that doesn't matter to the server
+ String publicKey = Base64.getMimeEncoder().encodeToString(playerKeyPair.publicKey().data().key().getEncoded());
+ byte[] publicKeySignature = playerKeyPair.publicKey().data().keySignature();
+ long expiresAt = playerKeyPair.publicKey().data().expiresAt().toEpochMilli();
+
+ TokenRequest.KeyPairInfo keyPairInfo = new TokenRequest.KeyPairInfo(Objects.requireNonNull(CLIENT.getSession().getUuidOrNull()), publicKey, publicKeySignature, expiresAt);
+ TokenRequest.SignedData signedData = Objects.requireNonNull(getRandomSignedData(playerKeyPair.privateKey()));
+ TokenRequest tokenRequest = new TokenRequest(keyPairInfo, signedData, SkyblockerMod.SKYBLOCKER_MOD.getMetadata().getId(), MINECRAFT_VERSION, SkyblockerMod.VERSION);
+
+ String request = SkyblockerMod.GSON.toJson(TokenRequest.CODEC.encodeStart(JsonOps.INSTANCE, tokenRequest).getOrThrow());
+
+ try {
+ tokenInfo = TokenInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(Http.sendPostRequest(AUTH_URL, request, CONTENT_TYPE))).getOrThrow();
+ int refreshAtTicks = (int) (((tokenInfo.expiresAt() - tokenInfo.issuedAt()) / 1000L) - 300L) * 20; //Refresh 5 minutes before expiry date
+
+ Scheduler.INSTANCE.schedule(ApiAuthentication::updateToken, refreshAtTicks, true);
+ } catch (Exception e) {
+ //Try again in 1 minute
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.authFailure"), 60 * 20, "[Skyblocker Api Auth] Failed to refresh the api token! Some features might not work :(", e);
+ }
+ } else {
+ //The Minecraft Services API is probably down so we will retry in 5 minutes, either that or your access token has expired (game open for 24h) and you need to restart.
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.noProfileKeys"), 300 * 20, "[Skyblocker Api Auth] Failed to fetch profile keys! Some features may not work temporarily :( (Has your game been open for more than 24 hours? If so restart.)");
+ }
+ }).exceptionally(throwable -> {
+ //Try again in 1 minute
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.authFailure"), 60 * 20, "[Skyblocker Api Auth] Encountered an unexpected exception while refreshing the api token!", throwable);
+
+ return null;
+ });
+ }
+
+ private static TokenRequest.SignedData getRandomSignedData(PrivateKey privateKey) {
+ try {
+ Signature signature = Signature.getInstance(ALGORITHM);
+ UUID uuid = UUID.randomUUID();
+ ByteBuffer buf = ByteBuffer.allocate(16)
+ .putLong(uuid.getMostSignificantBits())
+ .putLong(uuid.getLeastSignificantBits());
+
+ signature.initSign(privateKey);
+ signature.update(buf.array());
+
+ byte[] signedData = signature.sign();
+
+ return new TokenRequest.SignedData(buf.array(), signedData);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Api Auth] Failed to sign random data!", e);
+ }
+
+ //This should never ever be the case, since we are signing data that is not invalid in any case
+ return null;
+ }
+
+ private static void logErrorAndScheduleRetry(Text warningMessage, int retryAfter, String logMessage, Object... logArgs) {
+ LOGGER.error(logMessage, logArgs);
+ Scheduler.INSTANCE.schedule(ApiAuthentication::updateToken, retryAfter, true);
+
+ if (CLIENT.player != null) CLIENT.player.sendMessage(Constants.PREFIX.get().append(warningMessage));
+ }
+
+ @Nullable
+ public static String getToken() {
+ return tokenInfo != null ? tokenInfo.token() : null;
+ }
+
+ private record TokenRequest(KeyPairInfo keyPairInfo, SignedData signedData, String mod, String minecraftVersion, String modVersion) {
+ private static final Codec<TokenRequest> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ KeyPairInfo.CODEC.fieldOf("keyPair").forGetter(TokenRequest::keyPairInfo),
+ SignedData.CODEC.fieldOf("signedData").forGetter(TokenRequest::signedData),
+ Codec.STRING.fieldOf("mod").forGetter(TokenRequest::mod),
+ Codec.STRING.fieldOf("minecraftVersion").forGetter(TokenRequest::minecraftVersion),
+ Codec.STRING.fieldOf("modVersion").forGetter(TokenRequest::modVersion))
+ .apply(instance, TokenRequest::new));
+
+ private record KeyPairInfo(UUID uuid, String publicKey, byte[] publicKeySignature, long expiresAt) {
+ private static final Codec<KeyPairInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Uuids.STRING_CODEC.fieldOf("uuid").forGetter(KeyPairInfo::uuid),
+ Codec.STRING.fieldOf("publicKey").forGetter(KeyPairInfo::publicKey),
+ Codecs.BASE_64.fieldOf("publicKeySignature").forGetter(KeyPairInfo::publicKeySignature),
+ Codec.LONG.fieldOf("expiresAt").forGetter(KeyPairInfo::expiresAt))
+ .apply(instance, KeyPairInfo::new));
+ }
+
+ private record SignedData(byte[] original, byte[] signed) {
+ private static final Codec<SignedData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codecs.BASE_64.fieldOf("original").forGetter(SignedData::original),
+ Codecs.BASE_64.fieldOf("signed").forGetter(SignedData::signed))
+ .apply(instance, SignedData::new));
+ }
+ }
+
+ private record TokenInfo(String token, long issuedAt, long expiresAt) {
+ private static final Codec<TokenInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("token").forGetter(TokenInfo::token),
+ Codec.LONG.fieldOf("issuedAt").forGetter(TokenInfo::issuedAt),
+ Codec.LONG.fieldOf("expiresAt").forGetter(TokenInfo::expiresAt))
+ .apply(instance, TokenInfo::new));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java
index 871eac78..99db0316 100644
--- a/src/main/java/de/hysky/skyblocker/utils/Http.java
+++ b/src/main/java/de/hysky/skyblocker/utils/Http.java
@@ -17,6 +17,7 @@ import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import de.hysky.skyblocker.SkyblockerMod;
import net.minecraft.SharedConstants;
@@ -33,15 +34,18 @@ public class Http {
.followRedirects(Redirect.NORMAL)
.build();
- private static ApiResponse sendCacheableGetRequest(String url) throws IOException, InterruptedException {
- HttpRequest request = HttpRequest.newBuilder()
+ private static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException {
+ HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.GET()
.header("Accept", "application/json")
.header("Accept-Encoding", "gzip, deflate")
.header("User-Agent", USER_AGENT)
.version(Version.HTTP_2)
- .uri(URI.create(url))
- .build();
+ .uri(URI.create(url));
+
+ if (token != null) requestBuilder.header("Token", token);
+
+ HttpRequest request = requestBuilder.build();
HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream());
InputStream decodedInputStream = getDecodedInputStream(response);
@@ -69,7 +73,7 @@ public class Http {
}
public static String sendGetRequest(String url) throws IOException, InterruptedException {
- return sendCacheableGetRequest(url).content();
+ return sendCacheableGetRequest(url, null).content();
}
public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException {
@@ -84,8 +88,26 @@ public class Http {
return response.headers();
}
+ public static String sendPostRequest(String url, String requestBody, String contentType) throws IOException, InterruptedException {
+ HttpRequest request = HttpRequest.newBuilder()
+ .POST(BodyPublishers.ofString(requestBody))
+ .header("Accept-Encoding", "gzip, deflate")
+ .header("Content-Type", contentType)
+ .header("User-Agent", USER_AGENT)
+ .version(Version.HTTP_2)
+ .uri(URI.create(url))
+ .build();
+
+ HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream());
+ InputStream decodedInputStream = getDecodedInputStream(response);
+
+ String responseBody = new String(decodedInputStream.readAllBytes());
+
+ return responseBody;
+ }
+
public static ApiResponse sendName2UuidRequest(String name) throws IOException, InterruptedException {
- return sendCacheableGetRequest(NAME_2_UUID + name);
+ return sendCacheableGetRequest(NAME_2_UUID + name, null);
}
/**
@@ -96,7 +118,7 @@ public class Http {
* @implNote the {@code v2} prefix is automatically added
*/
public static ApiResponse sendHypixelRequest(String endpoint, @NotNull String query) throws IOException, InterruptedException {
- return sendCacheableGetRequest(HYPIXEL_PROXY + endpoint + query);
+ return sendCacheableGetRequest(HYPIXEL_PROXY + endpoint + query, ApiAuthentication.getToken());
}
private static InputStream getDecodedInputStream(HttpResponse<InputStream> response) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java
index 0a1f238a..a786e79f 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java
@@ -18,10 +18,6 @@ public class ProfileUtils {
public static Map<String, ObjectLongPair<JsonObject>> players = new HashMap<>();
- public static void init() {
- updateProfile();
- }
-
public static CompletableFuture<JsonObject> updateProfile() {
return updateProfile(MinecraftClient.getInstance().getSession().getUsername());
}