From 58ecd35735a3a787f726ab2a9ef3e596e320f718 Mon Sep 17 00:00:00 2001 From: Eman Cickusic Date: Fri, 17 Jul 2026 10:38:01 +0200 Subject: [PATCH] Android: send Firebase App Check (Play Integrity) token to the proxy Client half of the abuse-control workstream (backend is in the App Check PR). Attaches an X-Firebase-AppCheck header to /analyze and /chat so the backend can, once enforcement is enabled, verify requests come from the genuine app. - App.kt (Application) + network/llm/AppCheckTokenProvider.kt: install the Play Integrity provider and fetch tokens. Dormant until google-services.json exists (no default FirebaseApp -> currentToken() returns null -> no header), so this does not change behavior or break the build before Firebase is configured. - Firebase BoM + appcheck deps; Google Services plugin applied only when google-services.json is present. Manifest registers the Application. - Header added in both proxy workflows (no public API change; existing tests unaffected since the token is null without Firebase). NOTE: unbuilt in the authoring environment (Google Maven dl.google.com is not reachable here, so Firebase artifacts could not download). Needs a normal-network build + google-services.json before merge. Verified by review against the Firebase App Check API. See documentation/15-backend-abuse-controls.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/build.gradle.kts | 11 +++++ app/src/main/AndroidManifest.xml | 1 + .../main/java/com/b2/ultraprocessed/App.kt | 16 +++++++ .../network/llm/AppCheckTokenProvider.kt | 42 +++++++++++++++++++ .../network/llm/ProxyFoodLabelLlmWorkflow.kt | 7 +++- .../network/llm/ProxyResultChatWorkflow.kt | 7 +++- build.gradle.kts | 1 + 7 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/b2/ultraprocessed/App.kt create mode 100644 app/src/main/java/com/b2/ultraprocessed/network/llm/AppCheckTokenProvider.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d211c93..1922962 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,12 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") } +// App Check (Play Integrity) needs the Google Services plugin, which requires google-services.json. +// Apply it only when that file is present so the build still works before Firebase is configured. +if (project.file("google-services.json").exists()) { + apply(plugin = "com.google.gms.google-services") +} + fun String.asBuildConfigStringLiteral(): String = "\"" + replace("\\", "\\\\").replace("\"", "\\\"") + "\"" @@ -409,6 +415,11 @@ dependencies { implementation("org.json:json:20240303") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0") + // Firebase App Check (Play Integrity) — attests requests come from the genuine app. + implementation(platform("com.google.firebase:firebase-bom:33.7.0")) + implementation("com.google.firebase:firebase-appcheck") + implementation("com.google.firebase:firebase-appcheck-playintegrity") + testImplementation("junit:junit:4.13.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4b02a24..27d35ab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ android:required="false" /> val call = client.newCall(request) diff --git a/app/src/main/java/com/b2/ultraprocessed/network/llm/ProxyResultChatWorkflow.kt b/app/src/main/java/com/b2/ultraprocessed/network/llm/ProxyResultChatWorkflow.kt index e9a1657..3a1890f 100644 --- a/app/src/main/java/com/b2/ultraprocessed/network/llm/ProxyResultChatWorkflow.kt +++ b/app/src/main/java/com/b2/ultraprocessed/network/llm/ProxyResultChatWorkflow.kt @@ -33,11 +33,14 @@ class ProxyResultChatWorkflow( .put("history", JSONArray(trimHistory(history).map { it.toJson() })) val url = "${baseUrl.trimEnd('/')}/chat" AnalysisTelemetry.event("proxy_chat_request_start url=$url") - val request = Request.Builder() + val requestBuilder = Request.Builder() .url(url) .header("Content-Type", "application/json") .post(payload.toString().toRequestBody(JSON_MEDIA_TYPE)) - .build() + AppCheckTokenProvider.currentToken()?.let { + requestBuilder.header(AppCheckTokenProvider.X_FIREBASE_APPCHECK_HEADER, it) + } + val request = requestBuilder.build() client.newCall(request).execute().use { response -> val raw = response.body?.string().orEmpty() AnalysisTelemetry.event("proxy_chat_response http=${response.code}") diff --git a/build.gradle.kts b/build.gradle.kts index f360e1d..419af41 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { id("com.android.application") version "9.1.1" apply false id("org.jetbrains.kotlin.android") version "2.2.10" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.2.10" apply false + id("com.google.gms.google-services") version "4.4.2" apply false }