From 538d716bc6030256ab03bd04891fdb1b7a58c65a Mon Sep 17 00:00:00 2001 From: Zijian Zhang <35801754+futrime@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:13:55 +0800 Subject: [PATCH] fix: use the embedded CurseForge API key in local debug builds Local builds had an empty curseforge.api_key, so every CurseForge request returned 403. Local debug builds now fall back to the same obfuscated key that LeviLauncher for Windows embeds. Release, beta and CI builds still use only local.properties. CurseForge 401/403 responses now say whether the key is missing or was rejected. --- app/build.gradle | 18 +++++++++++++++++- app/curseforge-debug-apikey.bin | 1 + .../core/curseforge/CurseForgeClient.java | 17 ++++++++++++++--- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 app/curseforge-debug-apikey.bin diff --git a/app/build.gradle b/app/build.gradle index b5efd0abc..ffe0e4d10 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,21 @@ discordApplicationId = new BigInteger(discordApplicationId).toString() def discordLargeImage = providers.gradleProperty('discord.large_image') .getOrElse(discordProperties.getProperty('large_image', '')).trim() +def curseForgeApiKey = localProperties.getProperty('curseforge.api_key', '').trim() + +// Fallback for local debug builds only: the same obfuscated key LeviLauncher +// for Windows embeds (base64, then XOR 0xAF). Release and beta builds, and +// every CI build, keep using the key from local.properties (CI fills it from +// a secret). +def curseForgeDebugApiKey = curseForgeApiKey +if (curseForgeDebugApiKey.isEmpty() && !System.getenv('CI')) { + def embeddedKeyFile = file('curseforge-debug-apikey.bin') + if (embeddedKeyFile.exists()) { + def deobfuscated = embeddedKeyFile.bytes.collect { (byte) (it ^ 0xAF) } as byte[] + curseForgeDebugApiKey = new String(Base64.decoder.decode(new String(deobfuscated, 'US-ASCII')), 'UTF-8').trim() + } +} + def getGitTag() { def tag = "unknown" try { @@ -60,7 +75,7 @@ android { versionCode getVersionCodeByTimestamp() versionName getGitTag() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - buildConfigField "String", "CURSEFORGE_API_KEY", "\"${localProperties.getProperty('curseforge.api_key', '')}\"" + buildConfigField "String", "CURSEFORGE_API_KEY", groovy.json.JsonOutput.toJson(curseForgeApiKey) buildConfigField 'long', 'DISCORD_APPLICATION_ID', "${discordApplicationId}L" buildConfigField 'String', 'DISCORD_LARGE_IMAGE', groovy.json.JsonOutput.toJson(discordLargeImage) ndk { @@ -115,6 +130,7 @@ android { debugSymbolLevel 'FULL' } buildConfigField "boolean", "IS_BETA", "false" + buildConfigField "String", "CURSEFORGE_API_KEY", groovy.json.JsonOutput.toJson(curseForgeDebugApiKey) } beta { initWith release diff --git a/app/curseforge-debug-apikey.bin b/app/curseforge-debug-apikey.bin new file mode 100644 index 000000000..84959af79 --- /dev/null +++ b/app/curseforge-debug-apikey.bin @@ -0,0 +1 @@ +åëåÇåëêØåèßãÍéÌšùÅõøùøýÄýÄÜüèÌØÎéõõúê–ßÌèÈ�áûý÷ÊëåÄýùÄœöžÌÖýÅéßËÃê›öøöšûúØÚËúý× \ No newline at end of file diff --git a/app/src/main/java/org/levimc/launcher/core/curseforge/CurseForgeClient.java b/app/src/main/java/org/levimc/launcher/core/curseforge/CurseForgeClient.java index 4e3a8dfa3..f87124ed0 100644 --- a/app/src/main/java/org/levimc/launcher/core/curseforge/CurseForgeClient.java +++ b/app/src/main/java/org/levimc/launcher/core/curseforge/CurseForgeClient.java @@ -50,6 +50,17 @@ public static synchronized CurseForgeClient getInstance() { return instance; } + private static IOException httpError(Response response) { + int code = response.code(); + if (code == 401 || code == 403) { + String reason = API_KEY == null || API_KEY.isEmpty() + ? "no CurseForge API key in this build (set curseforge.api_key in local.properties)" + : "CurseForge rejected the API key"; + return new IOException(reason + " (HTTP " + code + ")"); + } + return new IOException("Unexpected code " + response); + } + public interface CurseForgeCallback { void onSuccess(T result); void onError(Throwable t); @@ -88,7 +99,7 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) { if (!response.isSuccessful()) { - callback.onError(new IOException("Unexpected code " + response)); + callback.onError(httpError(response)); return; } @@ -120,7 +131,7 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) throws IOException { if (!response.isSuccessful()) { - callback.onError(new IOException("Unexpected code " + response)); + callback.onError(httpError(response)); return; } @@ -155,7 +166,7 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) { if (!response.isSuccessful()) { - callback.onError(new IOException("Unexpected code " + response)); + callback.onError(httpError(response)); return; }