aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
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/utils/ApiAuthentication.java
parentd6b220a8e42a1fc2dbc955779e86d199851b4674 (diff)
downloadSkyblocker-163905a63d840a4dd17b29bb53c6e7be0bf55c03.tar.gz
Skyblocker-163905a63d840a4dd17b29bb53c6e7be0bf55c03.tar.bz2
Skyblocker-163905a63d840a4dd17b29bb53c6e7be0bf55c03.zip
API Authentication
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java')
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java149
1 files changed, 149 insertions, 0 deletions
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));
+ }
+}