diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt index e9b309d7d1..50f108d143 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt @@ -298,7 +298,7 @@ private fun buildOdsaRequestPayload( requestType = Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_AUTH_API ) - val akaResponse = eapAkaService.performSimAkaAuth(eapRelayPacket, imsi, mccMnc) + val akaResponse = eapAkaService.performSimAkaAuth(eapRelayPacket, eapId) ?: return null val postBody = JSONObject().put("eap-relay-packet", akaResponse).toString() diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaService.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaService.kt index 47fae2cb31..ed64dcf00e 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaService.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaService.kt @@ -34,7 +34,7 @@ class EapAkaService(private val telephonyManager: TelephonyManager) { } @RequiresApi(Build.VERSION_CODES.N) - fun performSimAkaAuth(eapRelayBase64: String, imsi: String, mccMnc: String): String? { + fun performSimAkaAuth(eapRelayBase64: String, identity: String): String? { val eapPacket = Base64.decode(eapRelayBase64, Base64.DEFAULT) if (eapPacket.size < 12) return null @@ -101,7 +101,6 @@ class EapAkaService(private val telephonyManager: TelephonyManager) { val ck = extractTlv(1 + res.size + 1, iccBytes) ?: return null val ik = extractTlv(1 + res.size + 1 + ck.size + 1, iccBytes) ?: return null - val identity = buildEapId(mccMnc, imsi) val identityBytes = identity.toByteArray(StandardCharsets.UTF_8) val keys = Fips186Prf.deriveKeys(identityBytes, ik, ck) @@ -127,19 +126,8 @@ class EapAkaService(private val telephonyManager: TelephonyManager) { } } - fun buildEapId(mccMnc: String, imsi: String, realm: String? = null): String { - val mcc = mccMnc.substring(0, 3) - var mnc = mccMnc.substring(3) - if (mnc.length == 2) mnc = "0$mnc" // Zero-pad 2-digit MNCs - val defaultRealm = "nai.epc.mnc$mnc.mcc$mcc.3gppnetwork.org" - val resolvedRealm = when { - realm.isNullOrBlank() -> defaultRealm - realm == "nai.epc" -> defaultRealm - realm.contains(".mnc") && realm.contains(".mcc") && realm.contains("3gppnetwork.org") -> realm - else -> realm - } - return "0$imsi@$resolvedRealm" - } + fun buildEapId(mccMnc: String, imsi: String, realm: String? = null): String = + buildEapIdentity(mccMnc, imsi, realm) private fun extractTlv(index: Int, data: ByteArray): ByteArray? { if (index >= data.size) return null @@ -209,3 +197,17 @@ class EapAkaService(private val telephonyManager: TelephonyManager) { null } } + +internal fun buildEapIdentity(mccMnc: String, imsi: String, realm: String? = null): String { + val mcc = mccMnc.substring(0, 3) + var mnc = mccMnc.substring(3) + if (mnc.length == 2) mnc = "0$mnc" // Zero-pad 2-digit MNCs + val defaultRealm = "nai.epc.mnc$mnc.mcc$mcc.3gppnetwork.org" + val resolvedRealm = when { + realm.isNullOrBlank() -> defaultRealm + realm == "nai.epc" -> defaultRealm + realm.contains(".mnc") && realm.contains(".mcc") && realm.contains("3gppnetwork.org") -> realm + else -> realm + } + return "0$imsi@$resolvedRealm" +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt index f5966f6691..6fc5060b46 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt @@ -25,6 +25,9 @@ fun ServiceEntitlementRequest.builder( null }, terminalId = telephonyManager.imei, + terminalVendor = Build.MANUFACTURER.orEmpty(), + terminalModel = Build.MODEL.orEmpty(), + terminalSoftwareVersion = Build.VERSION.RELEASE.orEmpty(), groupIdLevel1 = runCatching { telephonyManager.groupIdLevel1 }.getOrNull(), eapId = eapId, appIds = appIds, @@ -34,9 +37,9 @@ fun ServiceEntitlementRequest.builder( fun ServiceEntitlementRequest.userAgent(context: Context): String { val packageVersion = context.packageManager.getPackageInfo(context.packageName, 0).versionName.orEmpty() - val vendor = terminal_vendor.take(4) - val model = terminal_model.take(10) - val swVersion = terminal_software_version.take(20) + val vendor = resolveTerminalValue(terminal_vendor, Build.MANUFACTURER.orEmpty(), 4) + val model = resolveTerminalValue(terminal_model, Build.MODEL.orEmpty(), 10) + val swVersion = resolveTerminalValue(terminal_software_version, Build.VERSION.RELEASE.orEmpty(), 20) return "PRD-TS43 term-$vendor/$model /$packageVersion OS-Android/$swVersion" } @@ -52,6 +55,9 @@ fun OdsaOperation.builder( null }, terminalId = telephonyManager.imei, + terminalVendor = Build.MANUFACTURER.orEmpty(), + terminalModel = Build.MODEL.orEmpty(), + terminalSoftwareVersion = Build.VERSION.RELEASE.orEmpty(), groupIdLevel1 = runCatching { telephonyManager.groupIdLevel1 }.getOrNull(), eapId = "", // Not needed for ODSA appIds = appIds, @@ -63,6 +69,9 @@ class ServiceEntitlementBuilder( private val imsi: String, private val iccid: String?, private val terminalId: String?, + private val terminalVendor: String, + private val terminalModel: String, + private val terminalSoftwareVersion: String, private val groupIdLevel1: String?, private val eapId: String, private val appIds: List, @@ -73,9 +82,9 @@ class ServiceEntitlementBuilder( val baseUrl = entitlementUrl.toHttpUrl() // GMS truncates these fields: vendor (4), model (10), sw_version (20) - val vendor = req.terminal_vendor.take(4) - val model = req.terminal_model.take(10) - val swVersion = req.terminal_software_version.take(20) + val vendor = resolveTerminalValue(req.terminal_vendor, terminalVendor, 4) + val model = resolveTerminalValue(req.terminal_model, terminalModel, 10) + val swVersion = resolveTerminalValue(req.terminal_software_version, terminalSoftwareVersion, 20) return baseUrl.newBuilder().apply { when { @@ -92,7 +101,8 @@ class ServiceEntitlementBuilder( addQueryParameter("EAP_ID", eapId) } } - addQueryParameter("terminal_id", terminalId ?: req.terminal_id) + val resolvedTerminalId = req.terminal_id.takeIf { it.isNotEmpty() } ?: terminalId.orEmpty() + addQueryParameter("terminal_id", resolvedTerminalId) if (req.gid1.isNotEmpty()) { addQueryParameter("GID1", req.gid1) } else if ((req.entitlement_version.toBigDecimalOrNull()?.toInt() ?: 0) >= 12) { @@ -203,3 +213,10 @@ class ServiceEntitlementBuilder( }.build() } } + +internal fun resolveTerminalValue(requestValue: String, deviceFallback: String, maxLength: Int): String { + return requestValue + .takeIf { it.isNotEmpty() } + ?.take(maxLength) + ?: deviceFallback.take(maxLength) +} diff --git a/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaIdentityTest.kt b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaIdentityTest.kt new file mode 100644 index 0000000000..655a5d359c --- /dev/null +++ b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaIdentityTest.kt @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.verification.ts43 + +import org.junit.Assert.assertEquals +import org.junit.Test + +class EapAkaIdentityTest { + @Test + fun buildEapIdentity_usesDefaultRealm() { + assertEquals( + "0234107813240779@nai.epc.mnc010.mcc234.3gppnetwork.org", + buildEapIdentity("23410", "234107813240779") + ) + } + + @Test + fun buildEapIdentity_preservesChallengeRealm() { + assertEquals( + "0234107813240779@wlan.mnc010.mcc234.3gppnetwork.org", + buildEapIdentity( + "23410", + "234107813240779", + "wlan.mnc010.mcc234.3gppnetwork.org" + ) + ) + } +} diff --git a/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementBuilderTest.kt b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementBuilderTest.kt new file mode 100644 index 0000000000..ebc347f3e1 --- /dev/null +++ b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementBuilderTest.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.verification.ts43 + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.microg.gms.constellation.core.proto.ServiceEntitlementRequest + +class ServiceEntitlementBuilderTest { + + @Test + fun buildBaseUrl_explicitTerminalIdOverridesDeviceFallback() { + val request = ServiceEntitlementRequest(terminal_id = "request-terminal-id") + val url = builder(request, deviceTerminalId = "device-imei") + .buildBaseUrl("https://example.com/entitlement") + + assertEquals("request-terminal-id", url.queryParameter("terminal_id")) + } + + @Test + fun buildBaseUrl_emptyTerminalIdUsesDeviceFallback() { + val request = ServiceEntitlementRequest(terminal_id = "") + val url = builder(request, deviceTerminalId = "device-imei") + .buildBaseUrl("https://example.com/entitlement") + + assertEquals("device-imei", url.queryParameter("terminal_id")) + } + + private fun builder( + request: ServiceEntitlementRequest, + deviceTerminalId: String? + ) = ServiceEntitlementBuilder( + imsi = "", + iccid = null, + terminalId = deviceTerminalId, + terminalVendor = "", + terminalModel = "", + terminalSoftwareVersion = "", + groupIdLevel1 = null, + eapId = "", + appIds = listOf("ap2014"), + req = request + ) +} diff --git a/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementTerminalMetadataTest.kt b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementTerminalMetadataTest.kt new file mode 100644 index 0000000000..383334f74f --- /dev/null +++ b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementTerminalMetadataTest.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.verification.ts43 + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.microg.gms.constellation.core.proto.ServiceEntitlementRequest + +class ServiceEntitlementTerminalMetadataTest { + + @Test + fun buildBaseUrl_usesDeviceMetadataWhenRequestOmitsIt() { + val url = builder(ServiceEntitlementRequest()).buildBaseUrl("https://example.com/entitlement") + + assertEquals("Acme", url.queryParameter("terminal_vendor")) + assertEquals("Model-1234", url.queryParameter("terminal_model")) + assertEquals("Android-Release-1234", url.queryParameter("terminal_sw_version")) + } + + @Test + fun buildBaseUrl_explicitRequestMetadataTakesPrecedenceAndIsTruncated() { + val request = ServiceEntitlementRequest( + terminal_vendor = "RequestVendor", + terminal_model = "RequestModelLong", + terminal_software_version = "RequestSoftwareVersionThatIsLong" + ) + val url = builder(request).buildBaseUrl("https://example.com/entitlement") + + assertEquals("Requ", url.queryParameter("terminal_vendor")) + assertEquals("RequestMod", url.queryParameter("terminal_model")) + assertEquals("RequestSoftwareVersi", url.queryParameter("terminal_sw_version")) + } + + @Test + fun resolveTerminalValue_fallsBackBeforeTruncating() { + assertEquals("Acme", resolveTerminalValue("", "AcmeDevices", 4)) + assertEquals("Requ", resolveTerminalValue("RequestVendor", "AcmeDevices", 4)) + } + + private fun builder(request: ServiceEntitlementRequest) = ServiceEntitlementBuilder( + imsi = "", + iccid = null, + terminalId = "device-imei", + terminalVendor = "AcmeDevices", + terminalModel = "Model-123456789", + terminalSoftwareVersion = "Android-Release-123456789", + groupIdLevel1 = null, + eapId = "", + appIds = listOf("ap2014"), + req = request + ) +}