Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
}

Expand All @@ -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,
Expand All @@ -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<String>,
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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"
)
)
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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
)
}