mirror of
https://github.com/microg/android_packages_apps_GmsCore
synced 2026-08-06 14:26:05 -04:00
Merge d0347e5b6e into 9a206ae115
This commit is contained in:
commit
5d816bc80e
74 changed files with 7041 additions and 0 deletions
40
play-services-asterism/build.gradle
Normal file
40
play-services-asterism/build.gradle
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.asterism"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
apply from: '../gradle/publish-android.gradle'
|
||||
|
||||
description = 'microG implementation of play-services-asterism'
|
||||
|
||||
dependencies {
|
||||
implementation project(':play-services-basement')
|
||||
|
||||
annotationProcessor project(":safe-parcel-processor")
|
||||
}
|
||||
46
play-services-asterism/core/build.gradle
Normal file
46
play-services-asterism/core/build.gradle
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'signing'
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.asterism.core"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 1.8
|
||||
}
|
||||
}
|
||||
|
||||
apply from: '../../gradle/publish-android.gradle'
|
||||
|
||||
description = 'microG service implementation for play-services-asterism'
|
||||
|
||||
dependencies {
|
||||
api project(':play-services-asterism')
|
||||
|
||||
implementation project(':play-services-base-core')
|
||||
implementation project(':play-services-constellation-core')
|
||||
}
|
||||
15
play-services-asterism/core/src/main/AndroidManifest.xml
Normal file
15
play-services-asterism/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ SPDX-FileCopyrightText: 2026, microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<service android:name=".AsterismApiService">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.asterism.service.START" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.google.android.gms.asterism
|
||||
|
||||
import org.microg.gms.constellation.core.proto.AsterismClient
|
||||
import org.microg.gms.constellation.core.proto.Consent
|
||||
import org.microg.gms.constellation.core.proto.ConsentVersion
|
||||
|
||||
val GetAsterismConsentRequest.asterismClient: AsterismClient
|
||||
get() = AsterismClient.fromValue(asterismClientValue) ?: AsterismClient.UNKNOWN_CLIENT
|
||||
|
||||
fun getAsterismConsentResponse(
|
||||
requestCode: Int,
|
||||
consentState: Consent,
|
||||
gmscoreIidToken: String?,
|
||||
fid: String?,
|
||||
consentVersion: ConsentVersion
|
||||
): GetAsterismConsentResponse {
|
||||
val consentStateValue =
|
||||
if (consentState == Consent.CONSENTED || consentState == Consent.CONSENT_UNKNOWN) {
|
||||
consentState.value
|
||||
} else {
|
||||
Consent.NO_CONSENT.value
|
||||
}
|
||||
|
||||
return GetAsterismConsentResponse(
|
||||
requestCode,
|
||||
consentStateValue,
|
||||
gmscoreIidToken,
|
||||
fid,
|
||||
consentVersion.value
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.google.android.gms.asterism
|
||||
|
||||
import org.microg.gms.constellation.core.proto.AsterismClient
|
||||
import org.microg.gms.constellation.core.proto.AsterismConsent.DeviceConsentVersion
|
||||
import org.microg.gms.constellation.core.proto.Consent
|
||||
import org.microg.gms.constellation.core.proto.ConsentSource
|
||||
import org.microg.gms.constellation.core.proto.ConsentVersion
|
||||
|
||||
enum class SetAsterismConsentRequestStatus(val value: Int) {
|
||||
RESOURCE_TOS(0),
|
||||
STATIC_STRING(1),
|
||||
RCS(2),
|
||||
ON_DEMAND(3),
|
||||
RESOURCE_TOS_FALLBACK(4);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): SetAsterismConsentRequestStatus =
|
||||
entries.find { it.value == value } ?: RESOURCE_TOS_FALLBACK
|
||||
}
|
||||
}
|
||||
|
||||
val SetAsterismConsentRequest.asterismClient: AsterismClient
|
||||
get() = AsterismClient.fromValue(asterismClientValue) ?: AsterismClient.UNKNOWN_CLIENT
|
||||
|
||||
val SetAsterismConsentRequest.consent: Consent
|
||||
get() = Consent.fromValue(consentValue) ?: Consent.CONSENT_UNKNOWN
|
||||
|
||||
val SetAsterismConsentRequest.consentVersion: ConsentVersion?
|
||||
get() = ConsentVersion.fromValue(consentVersionValue)
|
||||
|
||||
val SetAsterismConsentRequest.deviceConsentSource: ConsentSource
|
||||
get() = ConsentSource.fromValue(deviceConsentSourceValue) ?: ConsentSource.SOURCE_UNSPECIFIED
|
||||
|
||||
val SetAsterismConsentRequest.deviceConsentVersion: DeviceConsentVersion
|
||||
get() = DeviceConsentVersion.fromValue(deviceConsentVersionValue).let {
|
||||
if (it == null || it == DeviceConsentVersion.UNKNOWN) {
|
||||
DeviceConsentVersion.PHONE_VERIFICATION_DEFAULT
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val SetAsterismConsentRequest.status: SetAsterismConsentRequestStatus
|
||||
get() = SetAsterismConsentRequestStatus.fromValue(statusValue)
|
||||
|
||||
fun SetAsterismConsentRequest.isDevicePnvrFlow(): Boolean {
|
||||
return asterismClient == AsterismClient.CONSTELLATION &&
|
||||
deviceConsentSourceValue > 0 &&
|
||||
deviceConsentVersionValue > 0
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package org.microg.gms.asterism.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.google.android.gms.asterism.GetAsterismConsentRequest
|
||||
import com.google.android.gms.asterism.SetAsterismConsentRequest
|
||||
import com.google.android.gms.asterism.internal.IAsterismApiService
|
||||
import com.google.android.gms.asterism.internal.IAsterismCallbacks
|
||||
import com.google.android.gms.common.Feature
|
||||
import com.google.android.gms.common.internal.ConnectionInfo
|
||||
import com.google.android.gms.common.internal.GetServiceRequest
|
||||
import com.google.android.gms.common.internal.IGmsCallbacks
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.microg.gms.BaseService
|
||||
import org.microg.gms.common.GmsService
|
||||
import org.microg.gms.common.PackageUtils
|
||||
|
||||
private const val TAG = "AsterismApiService"
|
||||
|
||||
class AsterismApiService : BaseService(TAG, GmsService.ASTERISM) {
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun handleServiceRequest(
|
||||
callback: IGmsCallbacks?,
|
||||
request: GetServiceRequest?,
|
||||
service: GmsService?
|
||||
) {
|
||||
val packageName = PackageUtils.getAndCheckCallingPackage(this, request?.packageName)
|
||||
if (!PackageUtils.isGooglePackage(this, packageName)) {
|
||||
throw SecurityException("$packageName is not a Google package")
|
||||
}
|
||||
callback!!.onPostInitCompleteWithConnectionInfo(
|
||||
0,
|
||||
AsterismApiServiceImpl(this, serviceScope).asBinder(),
|
||||
ConnectionInfo().apply {
|
||||
features = arrayOf(
|
||||
Feature("asterism_consent", 3),
|
||||
Feature("one_time_verification", 1),
|
||||
Feature("carrier_auth", 1),
|
||||
Feature("verify_phone_number", 2),
|
||||
Feature("get_iid_token", 1),
|
||||
Feature("get_pnv_capabilities", 1),
|
||||
Feature("ts43", 1),
|
||||
Feature("verify_phone_number_local_read", 1)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
serviceScope.cancel("AsterismApiService destroyed")
|
||||
}
|
||||
}
|
||||
|
||||
class AsterismApiServiceImpl(
|
||||
private val context: Context,
|
||||
private val serviceScope: CoroutineScope
|
||||
) : IAsterismApiService.Stub() {
|
||||
override fun getAsterismConsent(
|
||||
cb: IAsterismCallbacks?,
|
||||
request: GetAsterismConsentRequest?,
|
||||
) {
|
||||
Log.i(TAG, "getAsterismConsent(): $request")
|
||||
if (cb == null || request == null) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
serviceScope.launch { handleGetAsterismConsent(context, cb, request) }
|
||||
}
|
||||
|
||||
override fun setAsterismConsent(cb: IAsterismCallbacks?, request: SetAsterismConsentRequest?) {
|
||||
Log.i(TAG, "setAsterismConsent(): $request")
|
||||
if (cb == null || request == null) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
serviceScope.launch { handleSetAsterismConsent(context, cb, request) }
|
||||
}
|
||||
|
||||
override fun getIsPnvrConstellationDevice(cb: IAsterismCallbacks?) {
|
||||
Log.i(TAG, "getIsPnvrConstellationDevice()")
|
||||
if (cb == null) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
serviceScope.launch { handleGetIsPnvrConstellationDevice(context, cb) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
|
||||
package org.microg.gms.asterism.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.gms.asterism.GetAsterismConsentRequest
|
||||
import com.google.android.gms.asterism.asterismClient
|
||||
import com.google.android.gms.asterism.getAsterismConsentResponse
|
||||
import com.google.android.gms.asterism.internal.IAsterismCallbacks
|
||||
import com.google.android.gms.common.api.Status
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.constellation.core.ConstellationStateStore
|
||||
import org.microg.gms.constellation.core.RpcClient
|
||||
import org.microg.gms.constellation.core.authManager
|
||||
import org.microg.gms.constellation.core.proto.Consent
|
||||
import org.microg.gms.constellation.core.proto.ConsentVersion
|
||||
import org.microg.gms.constellation.core.proto.DeviceID
|
||||
import org.microg.gms.constellation.core.proto.GetConsentRequest
|
||||
import org.microg.gms.constellation.core.proto.RequestHeader
|
||||
import org.microg.gms.constellation.core.proto.RequestTrigger
|
||||
import org.microg.gms.constellation.core.proto.builder.buildRequestContext
|
||||
import org.microg.gms.constellation.core.proto.builder.invoke
|
||||
import java.util.UUID
|
||||
|
||||
private const val ASTERISM_TAG = "GetAsterismConsent"
|
||||
private const val PNVR_TAG = "GetIsPnvrDevice"
|
||||
|
||||
suspend fun handleGetAsterismConsent(
|
||||
context: Context,
|
||||
callbacks: IAsterismCallbacks,
|
||||
request: GetAsterismConsentRequest
|
||||
) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val authManager = context.authManager
|
||||
val buildContext = buildRequestContext(context, authManager)
|
||||
val response = RpcClient.phoneDeviceVerificationClient.GetConsent().execute(
|
||||
GetConsentRequest(
|
||||
device_id = DeviceID(context, buildContext.iidToken),
|
||||
header_ = RequestHeader(
|
||||
context,
|
||||
UUID.randomUUID().toString(),
|
||||
buildContext,
|
||||
"getConsent",
|
||||
RequestTrigger.Type.CONSENT_API_TRIGGER
|
||||
),
|
||||
asterism_client = request.asterismClient
|
||||
)
|
||||
)
|
||||
|
||||
val gaiaConsent = response.gaia_consents.find {
|
||||
it.asterism_client == request.asterismClient
|
||||
}
|
||||
val (consentValue, consentVersion) = if (gaiaConsent != null) {
|
||||
gaiaConsent.consent to gaiaConsent.consent_version
|
||||
} else {
|
||||
Consent.NO_CONSENT to ConsentVersion.CONSENT_VERSION_UNSPECIFIED
|
||||
}
|
||||
|
||||
callbacks.onConsentFetched(
|
||||
Status.SUCCESS,
|
||||
getAsterismConsentResponse(
|
||||
request.requestCode,
|
||||
consentValue,
|
||||
buildContext.iidToken,
|
||||
authManager.getFid(),
|
||||
consentVersion
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(ASTERISM_TAG, "getAsterismConsent failed", e)
|
||||
callbacks.onConsentFetched(
|
||||
Status.INTERNAL_ERROR,
|
||||
getAsterismConsentResponse(
|
||||
request.requestCode,
|
||||
Consent.CONSENT_UNKNOWN,
|
||||
null,
|
||||
null,
|
||||
ConsentVersion.CONSENT_VERSION_UNSPECIFIED
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleGetIsPnvrConstellationDevice(
|
||||
context: Context,
|
||||
callbacks: IAsterismCallbacks
|
||||
) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val consentValue = ConstellationStateStore.loadPnvrNoticeConsent(context)
|
||||
val isPnvrDevice = consentValue == Consent.CONSENTED || consentValue == Consent.NO_CONSENT
|
||||
|
||||
callbacks.onIsPnvrConstellationDevice(Status.SUCCESS, isPnvrDevice)
|
||||
} catch (e: Exception) {
|
||||
Log.e(PNVR_TAG, "getIsPnvrConstellationDevice failed", e)
|
||||
callbacks.onIsPnvrConstellationDevice(Status.INTERNAL_ERROR, false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
|
||||
package org.microg.gms.asterism.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.gms.asterism.SetAsterismConsentRequest
|
||||
import com.google.android.gms.asterism.SetAsterismConsentRequestStatus
|
||||
import com.google.android.gms.asterism.SetAsterismConsentResponse
|
||||
import com.google.android.gms.asterism.asterismClient
|
||||
import com.google.android.gms.asterism.consent
|
||||
import com.google.android.gms.asterism.consentVersion
|
||||
import com.google.android.gms.asterism.deviceConsentSource
|
||||
import com.google.android.gms.asterism.deviceConsentVersion
|
||||
import com.google.android.gms.asterism.internal.IAsterismCallbacks
|
||||
import com.google.android.gms.asterism.isDevicePnvrFlow
|
||||
import com.google.android.gms.asterism.status
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.squareup.wire.GrpcException
|
||||
import com.squareup.wire.GrpcStatus
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.microg.gms.constellation.core.ConstellationStateStore
|
||||
import org.microg.gms.constellation.core.RpcClient
|
||||
import org.microg.gms.constellation.core.authManager
|
||||
import org.microg.gms.constellation.core.proto.AsterismClient
|
||||
import org.microg.gms.constellation.core.proto.AsterismConsent
|
||||
import org.microg.gms.constellation.core.proto.AsterismConsent.DeviceConsentVersion
|
||||
import org.microg.gms.constellation.core.proto.AuditToken
|
||||
import org.microg.gms.constellation.core.proto.Consent
|
||||
import org.microg.gms.constellation.core.proto.ConsentVersion
|
||||
import org.microg.gms.constellation.core.proto.OnDemandConsent
|
||||
import org.microg.gms.constellation.core.proto.Param
|
||||
import org.microg.gms.constellation.core.proto.RcsConsent
|
||||
import org.microg.gms.constellation.core.proto.RequestHeader
|
||||
import org.microg.gms.constellation.core.proto.RequestTrigger
|
||||
import org.microg.gms.constellation.core.proto.SetConsentRequest
|
||||
import org.microg.gms.constellation.core.proto.builder.buildRequestContext
|
||||
import org.microg.gms.constellation.core.proto.builder.generate
|
||||
import org.microg.gms.constellation.core.proto.builder.getList
|
||||
import org.microg.gms.constellation.core.proto.builder.invoke
|
||||
import java.util.UUID
|
||||
|
||||
private const val TAG = "SetAsterismConsent"
|
||||
|
||||
suspend fun handleSetAsterismConsent(
|
||||
context: Context,
|
||||
callbacks: IAsterismCallbacks,
|
||||
request: SetAsterismConsentRequest
|
||||
) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (request.consent == Consent.CONSENT_UNKNOWN) {
|
||||
Log.e(TAG, "Invalid consentValue: ${request.consentValue}")
|
||||
callbacks.onConsentRegistered(
|
||||
Status.INTERNAL_ERROR,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
if (request.asterismClient != AsterismClient.CONSTELLATION &&
|
||||
request.asterismClient != AsterismClient.RCS
|
||||
) {
|
||||
Log.e(TAG, "Invalid asterismClientValue: ${request.asterismClientValue}")
|
||||
callbacks.onConsentRegistered(
|
||||
Status.INTERNAL_ERROR,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val isRcsSpecificConsentVersion = request.consentVersion in listOf(
|
||||
ConsentVersion.RCS_CONSENT,
|
||||
ConsentVersion.RCS_DEFAULT_ON_LEGAL_FYI,
|
||||
ConsentVersion.RCS_DEFAULT_ON_OUT_OF_BOX
|
||||
)
|
||||
val hasRcsFastPath = request.asterismClient == AsterismClient.RCS &&
|
||||
request.consentVersion in listOf(
|
||||
ConsentVersion.RCS_DEFAULT_ON_LEGAL_FYI,
|
||||
ConsentVersion.RCS_DEFAULT_ON_OUT_OF_BOX,
|
||||
ConsentVersion.RCS_DEFAULT_ON_LEGAL_FYI_IN_SETTINGS
|
||||
)
|
||||
|
||||
if (request.asterismClient != AsterismClient.RCS && isRcsSpecificConsentVersion) {
|
||||
Log.e(TAG, "RCS-only consent version cannot be used with non-RCS client")
|
||||
callbacks.onConsentRegistered(
|
||||
Status.INTERNAL_ERROR,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val apiParams = Param.getList(request.extras)
|
||||
|
||||
var deviceConsent: AsterismConsent? = null
|
||||
var rcsConsent: RcsConsent? = null
|
||||
var onDemandConsent: OnDemandConsent? = null
|
||||
var consentVersion: ConsentVersion? = null
|
||||
|
||||
when {
|
||||
request.isDevicePnvrFlow() -> {
|
||||
deviceConsent = AsterismConsent(
|
||||
consent = request.consent,
|
||||
consent_version = request.deviceConsentVersion,
|
||||
consent_source = request.deviceConsentSource
|
||||
)
|
||||
}
|
||||
|
||||
request.status == SetAsterismConsentRequestStatus.ON_DEMAND -> {
|
||||
if (
|
||||
request.language.isNullOrBlank() ||
|
||||
request.field11.isNullOrBlank() ||
|
||||
request.accountName.isNullOrBlank() ||
|
||||
request.consentVariant.isNullOrBlank() ||
|
||||
request.consentTrigger.isNullOrBlank()
|
||||
) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"ODC requires language, field11, accountName, consentVariant, and consentTrigger"
|
||||
)
|
||||
callbacks.onConsentRegistered(
|
||||
Status.INTERNAL_ERROR,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
onDemandConsent = OnDemandConsent(context, request, request.extras)
|
||||
if (onDemandConsent == null) {
|
||||
Log.e(TAG, "ODC Flow missing required fields, aborting.")
|
||||
callbacks.onConsentRegistered(
|
||||
Status.CANCELED,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (request.status == SetAsterismConsentRequestStatus.RESOURCE_TOS ||
|
||||
request.status == SetAsterismConsentRequestStatus.RESOURCE_TOS_FALLBACK
|
||||
) {
|
||||
if (request.tosResourceIds?.isEmpty() != false) {
|
||||
Log.w(TAG, "Resource ToS requires tosResourceIds")
|
||||
callbacks.onConsentRegistered(
|
||||
Status.INTERNAL_ERROR,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
} else if (!hasRcsFastPath &&
|
||||
(request.language.isNullOrBlank() || request.field11.isNullOrBlank())
|
||||
) {
|
||||
Log.w(TAG, "Static string ToS requires language and field11")
|
||||
Log.w(
|
||||
TAG,
|
||||
"language=${request.language}, field11=${request.field11}"
|
||||
)
|
||||
callbacks.onConsentRegistered(
|
||||
Status.INTERNAL_ERROR,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val rcsConsentVersion = if (request.consent == Consent.CONSENTED) {
|
||||
ConsentVersion.RCS_CONSENT
|
||||
} else {
|
||||
ConsentVersion.CONSENT_VERSION_UNSPECIFIED
|
||||
}
|
||||
|
||||
rcsConsent = RcsConsent(
|
||||
consent = request.consent,
|
||||
consent_version = rcsConsentVersion
|
||||
)
|
||||
consentVersion = request.consentVersion
|
||||
}
|
||||
}
|
||||
|
||||
val needsAuditRecord = deviceConsent != null || onDemandConsent != null
|
||||
val auditRecordBytes = if (needsAuditRecord) {
|
||||
AuditToken.generate().encode().toByteString()
|
||||
} else ByteString.EMPTY
|
||||
|
||||
val authManager = context.authManager
|
||||
val buildContext = buildRequestContext(context, authManager)
|
||||
val sessionId = UUID.randomUUID().toString()
|
||||
|
||||
val protoRequest = SetConsentRequest(
|
||||
header_ = RequestHeader(
|
||||
context,
|
||||
sessionId,
|
||||
buildContext,
|
||||
"setConsent",
|
||||
RequestTrigger.Type.CONSENT_API_TRIGGER
|
||||
),
|
||||
asterism_client = request.asterismClient,
|
||||
device_consent = deviceConsent,
|
||||
rcs_consent = rcsConsent,
|
||||
on_demand_consent = onDemandConsent,
|
||||
consent_version = consentVersion ?: ConsentVersion.CONSENT_VERSION_UNSPECIFIED,
|
||||
api_params = apiParams,
|
||||
audit_record = auditRecordBytes
|
||||
)
|
||||
|
||||
try {
|
||||
RpcClient.phoneDeviceVerificationClient.SetConsent().execute(protoRequest)
|
||||
} catch (e: GrpcException) {
|
||||
if (e.grpcStatus == GrpcStatus.PERMISSION_DENIED ||
|
||||
e.grpcStatus == GrpcStatus.UNAUTHENTICATED
|
||||
) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache..."
|
||||
)
|
||||
ConstellationStateStore.clearDroidGuardToken(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
if (deviceConsent?.consent_version == DeviceConsentVersion.PHONE_VERIFICATION_REACHABILITY_INTL_SMS_CALLS) {
|
||||
ConstellationStateStore.storePnvrNotice(
|
||||
context,
|
||||
deviceConsent.consent,
|
||||
deviceConsent.consent_source,
|
||||
deviceConsent.consent_version
|
||||
)
|
||||
}
|
||||
|
||||
callbacks.onConsentRegistered(
|
||||
Status.SUCCESS,
|
||||
SetAsterismConsentResponse(
|
||||
request.requestCode,
|
||||
buildContext.iidToken,
|
||||
authManager.getFid()
|
||||
)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "setAsterismConsent failed", e)
|
||||
callbacks.onConsentRegistered(
|
||||
Status.INTERNAL_ERROR,
|
||||
SetAsterismConsentResponse(request.requestCode, "", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package org.microg.gms.constellation.core.proto.builder
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.google.android.gms.asterism.SetAsterismConsentRequest
|
||||
import com.google.android.gms.asterism.SetAsterismConsentRequestStatus
|
||||
import com.google.android.gms.asterism.consent
|
||||
import com.google.android.gms.asterism.status
|
||||
import org.microg.gms.constellation.core.proto.Consent
|
||||
import org.microg.gms.constellation.core.proto.GaiaToken
|
||||
import org.microg.gms.constellation.core.proto.OnDemandConsent
|
||||
|
||||
private const val TAG = "OnDemandConsent"
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
operator fun OnDemandConsent.Companion.invoke(
|
||||
context: Context,
|
||||
request: SetAsterismConsentRequest,
|
||||
extras: Bundle?
|
||||
): OnDemandConsent? {
|
||||
if (request.status != SetAsterismConsentRequestStatus.ON_DEMAND) return null
|
||||
|
||||
val consentVariant = request.consentVariant
|
||||
val consentTrigger = request.consentTrigger
|
||||
val accountName = request.accountName
|
||||
|
||||
if (
|
||||
accountName.isNullOrBlank() ||
|
||||
consentVariant.isNullOrBlank() ||
|
||||
consentTrigger.isNullOrBlank()
|
||||
) {
|
||||
Log.w(TAG, "ODC missing required request fields")
|
||||
return null
|
||||
}
|
||||
|
||||
val gaiaAccessToken = try {
|
||||
val account = Account(accountName, "com.google")
|
||||
val accountManager = AccountManager.get(context)
|
||||
val future = accountManager.getAuthToken(
|
||||
account, "oauth2:https://www.googleapis.com/auth/numberer",
|
||||
null, false, null, null
|
||||
)
|
||||
future.result?.getString(AccountManager.KEY_AUTHTOKEN)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to get auth token for account $accountName: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
if (gaiaAccessToken.isNullOrBlank()) {
|
||||
Log.w(TAG, "ODC missing gaiaAccessToken")
|
||||
return null
|
||||
}
|
||||
|
||||
val consentValue = if (request.consent == Consent.CONSENTED) {
|
||||
Consent.CONSENTED
|
||||
} else {
|
||||
Consent.NO_CONSENT
|
||||
}
|
||||
|
||||
return OnDemandConsent(
|
||||
consent = consentValue,
|
||||
gaia_token = GaiaToken(token = gaiaAccessToken),
|
||||
consent_variant = consentVariant,
|
||||
trigger = consentTrigger
|
||||
)
|
||||
}
|
||||
17
play-services-asterism/src/main/AndroidManifest.xml
Normal file
17
play-services-asterism/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (C) 2013-2017 microG Project Team
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest />
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
parcelable GetAsterismConsentRequest;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
parcelable GetAsterismConsentResponse;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
parcelable SetAsterismConsentRequest;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
parcelable SetAsterismConsentResponse;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.google.android.gms.asterism.internal;
|
||||
|
||||
import com.google.android.gms.asterism.internal.IAsterismCallbacks;
|
||||
import com.google.android.gms.asterism.GetAsterismConsentRequest;
|
||||
import com.google.android.gms.asterism.SetAsterismConsentRequest;
|
||||
|
||||
interface IAsterismApiService {
|
||||
void getAsterismConsent(
|
||||
IAsterismCallbacks cb,
|
||||
in GetAsterismConsentRequest request
|
||||
);
|
||||
void setAsterismConsent(
|
||||
IAsterismCallbacks cb,
|
||||
in SetAsterismConsentRequest request
|
||||
);
|
||||
void getIsPnvrConstellationDevice(
|
||||
IAsterismCallbacks cb
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.google.android.gms.asterism.internal;
|
||||
|
||||
import com.google.android.gms.common.api.Status;
|
||||
|
||||
import com.google.android.gms.asterism.GetAsterismConsentResponse;
|
||||
import com.google.android.gms.asterism.SetAsterismConsentResponse;
|
||||
|
||||
interface IAsterismCallbacks {
|
||||
void onConsentFetched(in Status status, in GetAsterismConsentResponse response);
|
||||
void onConsentRegistered(in Status status, in SetAsterismConsentResponse response);
|
||||
void onIsPnvrConstellationDevice(in Status status, boolean isPnvrDevice);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class GetAsterismConsentRequest extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<GetAsterismConsentRequest> CREATOR =
|
||||
findCreator(GetAsterismConsentRequest.class);
|
||||
|
||||
@Field(1)
|
||||
public final int requestCode;
|
||||
|
||||
@Field(2)
|
||||
public final int asterismClientValue;
|
||||
|
||||
@Constructor
|
||||
public GetAsterismConsentRequest(
|
||||
@Param(1) int requestCode,
|
||||
@Param(2) int asterismClientValue
|
||||
) {
|
||||
this.requestCode = requestCode;
|
||||
this.asterismClientValue = asterismClientValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class GetAsterismConsentResponse extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<GetAsterismConsentResponse> CREATOR =
|
||||
findCreator(GetAsterismConsentResponse.class);
|
||||
|
||||
@Field(1)
|
||||
public final int requestCode;
|
||||
|
||||
@Field(2)
|
||||
public final int consentStateValue;
|
||||
|
||||
@Field(3)
|
||||
@Nullable
|
||||
public final String gmscoreIidToken;
|
||||
|
||||
@Field(4)
|
||||
@Nullable
|
||||
public final String fid;
|
||||
|
||||
@Field(5)
|
||||
public final int consentTypeValue;
|
||||
|
||||
@Constructor
|
||||
public GetAsterismConsentResponse(
|
||||
@Param(1) int requestCode,
|
||||
@Param(2) int consentStateValue,
|
||||
@Param(3) @Nullable String gmscoreIidToken,
|
||||
@Param(4) @Nullable String fid,
|
||||
@Param(5) int consentTypeValue
|
||||
) {
|
||||
this.requestCode = requestCode;
|
||||
this.consentStateValue = consentStateValue;
|
||||
this.gmscoreIidToken = gmscoreIidToken;
|
||||
this.fid = fid;
|
||||
this.consentTypeValue = consentTypeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
import org.microg.gms.utils.ToStringHelper;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class SetAsterismConsentRequest extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<SetAsterismConsentRequest> CREATOR =
|
||||
findCreator(SetAsterismConsentRequest.class);
|
||||
|
||||
@Field(1)
|
||||
public final int requestCode;
|
||||
|
||||
@Field(2)
|
||||
public final int asterismClientValue;
|
||||
|
||||
@Field(3)
|
||||
public final int flowContextValue;
|
||||
|
||||
@Field(4)
|
||||
@Nullable
|
||||
public final int[] tosResourceIds;
|
||||
|
||||
@Field(5)
|
||||
@Nullable
|
||||
public final Long timestamp;
|
||||
|
||||
@Field(6)
|
||||
public final int consentValue;
|
||||
|
||||
@Field(7)
|
||||
@Nullable
|
||||
public final Bundle extras;
|
||||
|
||||
@Field(8)
|
||||
public final int statusValue;
|
||||
|
||||
@Field(9)
|
||||
@Nullable
|
||||
public final String clientVersion;
|
||||
|
||||
@Field(10)
|
||||
@Nullable
|
||||
public final String language;
|
||||
|
||||
@Field(11)
|
||||
@Nullable
|
||||
public final String field11;
|
||||
|
||||
@Field(12)
|
||||
@Nullable
|
||||
public final String field12;
|
||||
|
||||
@Field(13)
|
||||
@Nullable
|
||||
public final String field13;
|
||||
|
||||
@Field(14)
|
||||
@Nullable
|
||||
public final String accountName;
|
||||
|
||||
@Field(15)
|
||||
@Nullable
|
||||
public final String consentVariant;
|
||||
|
||||
@Field(16)
|
||||
@Nullable
|
||||
public final String consentTrigger;
|
||||
|
||||
@Field(17)
|
||||
public final int consentVersionValue;
|
||||
|
||||
@Field(18)
|
||||
public final int deviceConsentSourceValue;
|
||||
|
||||
@Field(19)
|
||||
public final int deviceConsentVersionValue;
|
||||
|
||||
@Constructor
|
||||
public SetAsterismConsentRequest(
|
||||
@Param(1) int requestCode,
|
||||
@Param(2) int asterismClientValue,
|
||||
@Param(3) int flowContextValue,
|
||||
@Param(4) @Nullable int[] tosResourceIds,
|
||||
@Param(5) @Nullable Long timestamp,
|
||||
@Param(6) int consentValue,
|
||||
@Param(7) @Nullable Bundle extras,
|
||||
@Param(8) int statusValue,
|
||||
@Param(9) @Nullable String clientVersion,
|
||||
@Param(10) @Nullable String locale,
|
||||
@Param(11) @Nullable String field11,
|
||||
@Param(12) @Nullable String field12,
|
||||
@Param(13) @Nullable String field13,
|
||||
@Param(14) @Nullable String accountName,
|
||||
@Param(15) @Nullable String consentVariant,
|
||||
@Param(16) @Nullable String consentTrigger,
|
||||
@Param(17) int consentVersionValue,
|
||||
@Param(18) int deviceConsentSourceValue,
|
||||
@Param(19) int deviceConsentVersionValue
|
||||
) {
|
||||
this.requestCode = requestCode;
|
||||
this.asterismClientValue = asterismClientValue;
|
||||
this.flowContextValue = flowContextValue;
|
||||
this.tosResourceIds = tosResourceIds;
|
||||
this.timestamp = timestamp;
|
||||
this.consentValue = consentValue;
|
||||
this.extras = extras;
|
||||
this.statusValue = statusValue;
|
||||
this.clientVersion = clientVersion;
|
||||
this.language = locale;
|
||||
this.field11 = field11;
|
||||
this.field12 = field12;
|
||||
this.field13 = field13;
|
||||
this.accountName = accountName;
|
||||
this.consentVariant = consentVariant;
|
||||
this.consentTrigger = consentTrigger;
|
||||
this.consentVersionValue = consentVersionValue;
|
||||
this.deviceConsentSourceValue = deviceConsentSourceValue;
|
||||
this.deviceConsentVersionValue = deviceConsentVersionValue;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringHelper.name("SetAsterismConsentRequest")
|
||||
.field("requestCode", requestCode)
|
||||
.field("asterismClientValue", asterismClientValue)
|
||||
.field("flowContextValue", flowContextValue)
|
||||
.field("tosResourceIds", tosResourceIds)
|
||||
.field("timestamp", timestamp)
|
||||
.field("consentValue", consentValue)
|
||||
.field("extras", extras)
|
||||
.field("statusValue", statusValue)
|
||||
.field("clientVersion", clientVersion)
|
||||
.field("language", language)
|
||||
.field("field11", field11)
|
||||
.field("field12", field12)
|
||||
.field("field13", field13)
|
||||
.field("accountName", accountName)
|
||||
.field("consentVariant", consentVariant)
|
||||
.field("consentTrigger", consentTrigger)
|
||||
.field("consentVersionValue", consentVersionValue)
|
||||
.field("deviceConsentSourceValue", deviceConsentSourceValue)
|
||||
.field("deviceConsentVersionValue", deviceConsentVersionValue)
|
||||
.end();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.google.android.gms.asterism;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class SetAsterismConsentResponse extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<SetAsterismConsentResponse> CREATOR =
|
||||
findCreator(SetAsterismConsentResponse.class);
|
||||
|
||||
@Field(1)
|
||||
public final int requestCode;
|
||||
|
||||
@Field(2)
|
||||
@Nullable
|
||||
public final String gmscoreIidToken;
|
||||
|
||||
@Field(3)
|
||||
@Nullable
|
||||
public final String fid;
|
||||
|
||||
@Constructor
|
||||
public SetAsterismConsentResponse(
|
||||
@Param(1) int requestCode,
|
||||
@Param(2) @Nullable String gmscoreIidToken,
|
||||
@Param(3) @Nullable String fid
|
||||
) {
|
||||
this.requestCode = requestCode;
|
||||
this.gmscoreIidToken = gmscoreIidToken;
|
||||
this.fid = fid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
40
play-services-constellation/build.gradle
Normal file
40
play-services-constellation/build.gradle
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.constellation"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
apply from: '../gradle/publish-android.gradle'
|
||||
|
||||
description = 'microG implementation of play-services-constellation'
|
||||
|
||||
dependencies {
|
||||
implementation project(':play-services-basement')
|
||||
|
||||
annotationProcessor project(":safe-parcel-processor")
|
||||
}
|
||||
62
play-services-constellation/core/build.gradle
Normal file
62
play-services-constellation/core/build.gradle
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'signing'
|
||||
apply plugin: 'com.squareup.wire'
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.constellation.core"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 1.8
|
||||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
rpcRole = 'client'
|
||||
rpcCallStyle = 'suspending'
|
||||
}
|
||||
}
|
||||
|
||||
apply from: '../../gradle/publish-android.gradle'
|
||||
|
||||
description = 'microG service implementation for play-services-constellation'
|
||||
|
||||
dependencies {
|
||||
api project(':play-services-constellation')
|
||||
|
||||
implementation project(':play-services-base-core')
|
||||
implementation project(':play-services-iid')
|
||||
implementation project(':play-services-auth-base')
|
||||
|
||||
implementation project(':play-services-droidguard')
|
||||
implementation project(':play-services-tasks-ktx')
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:$okHttpVersion"
|
||||
api "com.squareup.wire:wire-runtime:$wireVersion"
|
||||
api "com.squareup.wire:wire-grpc-client:$wireVersion"
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ SPDX-FileCopyrightText: 2026, microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_SMS"
|
||||
tools:ignore="SmsAndCallLogPolicy" />
|
||||
<uses-permission
|
||||
android:name="android.permission.RECEIVE_SMS"
|
||||
tools:ignore="SmsAndCallLogPolicy" />
|
||||
<uses-permission
|
||||
android:name="android.permission.SEND_SMS"
|
||||
tools:ignore="SmsAndCallLogPolicy" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
|
||||
<application>
|
||||
<service android:name=".ConstellationApiService">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.constellation.service.START" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.google.android.gms.constellation
|
||||
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesResponse.VerificationCapability
|
||||
|
||||
enum class VerificationStatus(val value: Int) {
|
||||
SUPPORTED(1),
|
||||
UNSUPPORTED_CARRIER(2),
|
||||
UNSUPPORTED_API_VERSION(3),
|
||||
UNSUPPORTED_SIM_NOT_READY(4);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): VerificationStatus = when (value) {
|
||||
2 -> UNSUPPORTED_CARRIER
|
||||
3 -> UNSUPPORTED_API_VERSION
|
||||
4 -> UNSUPPORTED_SIM_NOT_READY
|
||||
else -> SUPPORTED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verificationCapability(
|
||||
verificationMethod: Int,
|
||||
status: VerificationStatus
|
||||
): VerificationCapability {
|
||||
return VerificationCapability(verificationMethod, status.value)
|
||||
}
|
||||
|
||||
val VerificationCapability.status: VerificationStatus
|
||||
get() = VerificationStatus.fromInt(statusValue)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.google.android.gms.constellation
|
||||
|
||||
import org.microg.gms.constellation.core.proto.VerificationMethod
|
||||
|
||||
val VerifyPhoneNumberRequest.verificationMethods: List<VerificationMethod>
|
||||
get() = verificationMethodsValues.mapNotNull { VerificationMethod.fromValue(it) }
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import com.google.android.gms.iid.InstanceID
|
||||
import com.squareup.wire.Instant
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
|
||||
val Context.authManager: AuthManager get() = AuthManager.get(this)
|
||||
|
||||
class AuthManager private constructor(context: Context) {
|
||||
private val context = context.applicationContext
|
||||
private val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "constellation_prefs"
|
||||
private const val KEY_PRIVATE = "private_key"
|
||||
private const val KEY_PUBLIC = "public_key"
|
||||
|
||||
// This is safe as the Context is immediately converted to the application context.
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Volatile
|
||||
private var instance: AuthManager? = null
|
||||
|
||||
fun get(context: Context): AuthManager = instance ?: synchronized(this) {
|
||||
instance ?: AuthManager(context).also { instance = it }
|
||||
}
|
||||
}
|
||||
|
||||
// GMS signing format: {iidToken}:{seconds}:{nanos}
|
||||
fun signIidTokenCompat(iidToken: String): Pair<ByteArray, Long> {
|
||||
val currentTimeMillis = System.currentTimeMillis()
|
||||
|
||||
val epochSecond = currentTimeMillis / 1000
|
||||
val nano = (currentTimeMillis % 1000) * 1_000_000
|
||||
|
||||
val content = "$iidToken:$epochSecond:$nano"
|
||||
return sign(content) to currentTimeMillis
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun signIidToken(iidToken: String): Pair<ByteArray, Instant> {
|
||||
val (bytes, millis) = signIidTokenCompat(iidToken)
|
||||
return bytes to Instant.ofEpochMilli(millis)
|
||||
}
|
||||
|
||||
fun getIidToken(projectNumber: String? = null): String {
|
||||
return try {
|
||||
val sender = projectNumber ?: IidTokenPhenotypes.DEFAULT_PROJECT_NUMBER
|
||||
InstanceID.getInstance(context).getToken(sender, "GCM")
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateKeyPair(): KeyPair {
|
||||
val privateKeyStr = sharedPrefs.getString(KEY_PRIVATE, null)
|
||||
val publicKeyStr = sharedPrefs.getString(KEY_PUBLIC, null)
|
||||
|
||||
if (privateKeyStr != null && publicKeyStr != null) {
|
||||
try {
|
||||
val kf = KeyFactory.getInstance("EC")
|
||||
val privateKey = kf.generatePrivate(
|
||||
PKCS8EncodedKeySpec(
|
||||
Base64.decode(
|
||||
privateKeyStr,
|
||||
Base64.DEFAULT
|
||||
)
|
||||
)
|
||||
)
|
||||
val publicKey = kf.generatePublic(
|
||||
X509EncodedKeySpec(
|
||||
Base64.decode(
|
||||
publicKeyStr,
|
||||
Base64.DEFAULT
|
||||
)
|
||||
)
|
||||
)
|
||||
return KeyPair(publicKey, privateKey)
|
||||
} catch (_: Exception) {
|
||||
// Fall through to regeneration on failure
|
||||
}
|
||||
}
|
||||
|
||||
val kpg = KeyPairGenerator.getInstance("EC")
|
||||
kpg.initialize(256)
|
||||
val kp = kpg.generateKeyPair()
|
||||
|
||||
sharedPrefs.edit {
|
||||
putString(KEY_PRIVATE, Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP))
|
||||
putString(KEY_PUBLIC, Base64.encodeToString(kp.public.encoded, Base64.NO_WRAP))
|
||||
}
|
||||
|
||||
return kp
|
||||
}
|
||||
|
||||
fun sign(content: String): ByteArray {
|
||||
return try {
|
||||
val kp = getOrCreateKeyPair()
|
||||
val signature = Signature.getInstance("SHA256withECDSA")
|
||||
signature.initSign(kp.private)
|
||||
signature.update(content.toByteArray(StandardCharsets.UTF_8))
|
||||
signature.sign()
|
||||
} catch (_: Exception) {
|
||||
ByteArray(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFid(): String = InstanceID.getInstance(context).id
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.google.android.gms.common.Feature
|
||||
import com.google.android.gms.common.api.ApiMetadata
|
||||
import com.google.android.gms.common.internal.ConnectionInfo
|
||||
import com.google.android.gms.common.internal.GetServiceRequest
|
||||
import com.google.android.gms.common.internal.IGmsCallbacks
|
||||
import com.google.android.gms.constellation.GetIidTokenRequest
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesRequest
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberRequest
|
||||
import com.google.android.gms.constellation.internal.IConstellationApiService
|
||||
import com.google.android.gms.constellation.internal.IConstellationCallbacks
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.microg.gms.BaseService
|
||||
import org.microg.gms.common.GmsService
|
||||
import org.microg.gms.common.PackageUtils
|
||||
|
||||
private const val TAG = "C11NApiService"
|
||||
|
||||
class ConstellationApiService : BaseService(TAG, GmsService.CONSTELLATION) {
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun handleServiceRequest(
|
||||
callback: IGmsCallbacks?,
|
||||
request: GetServiceRequest?,
|
||||
service: GmsService?
|
||||
) {
|
||||
val packageName = PackageUtils.getAndCheckCallingPackage(this, request?.packageName)
|
||||
if (!PackageUtils.isGooglePackage(this, packageName)) {
|
||||
throw SecurityException("$packageName is not a Google package")
|
||||
}
|
||||
callback!!.onPostInitCompleteWithConnectionInfo(
|
||||
0,
|
||||
ConstellationApiServiceImpl(this, packageName, serviceScope).asBinder(),
|
||||
ConnectionInfo().apply {
|
||||
features = arrayOf(
|
||||
Feature("asterism_consent", 3),
|
||||
Feature("one_time_verification", 1),
|
||||
Feature("carrier_auth", 1),
|
||||
Feature("verify_phone_number", 2),
|
||||
Feature("get_iid_token", 1),
|
||||
Feature("get_pnv_capabilities", 1),
|
||||
Feature("ts43", 1),
|
||||
Feature("verify_phone_number_local_read", 1)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
serviceScope.cancel("ConstellationApiService destroyed")
|
||||
}
|
||||
}
|
||||
|
||||
class ConstellationApiServiceImpl(
|
||||
private val context: Context,
|
||||
private val packageName: String?,
|
||||
private val serviceScope: CoroutineScope
|
||||
) : IConstellationApiService.Stub() {
|
||||
override fun verifyPhoneNumberV1(
|
||||
cb: IConstellationCallbacks?,
|
||||
bundle: Bundle?,
|
||||
apiMetadata: ApiMetadata?
|
||||
) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"verifyPhoneNumberV1(): mode=${bundle?.getInt("verification_mode")}, policy=${
|
||||
bundle?.getString("policy_id")
|
||||
}"
|
||||
)
|
||||
if (cb == null || bundle == null) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
serviceScope.launch {
|
||||
handleVerifyPhoneNumberV1(
|
||||
context,
|
||||
ConstellationCallbacksWrapper(cb),
|
||||
bundle,
|
||||
packageName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyPhoneNumberSingleUse(
|
||||
cb: IConstellationCallbacks?,
|
||||
bundle: Bundle?,
|
||||
apiMetadata: ApiMetadata?
|
||||
) {
|
||||
Log.i(TAG, "verifyPhoneNumberSingleUse()")
|
||||
if (cb == null || bundle == null) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
serviceScope.launch {
|
||||
handleVerifyPhoneNumberSingleUse(
|
||||
context,
|
||||
ConstellationCallbacksWrapper(cb),
|
||||
bundle,
|
||||
packageName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyPhoneNumber(
|
||||
cb: IConstellationCallbacks?,
|
||||
request: VerifyPhoneNumberRequest?,
|
||||
apiMetadata: ApiMetadata?
|
||||
) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"verifyPhoneNumber(): apiVersion=${request?.apiVersion}, policy=${request?.policyId}"
|
||||
)
|
||||
if (cb == null || request == null) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
serviceScope.launch {
|
||||
handleVerifyPhoneNumberRequest(
|
||||
context,
|
||||
ConstellationCallbacksWrapper(cb),
|
||||
request,
|
||||
packageName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIidToken(
|
||||
cb: IConstellationCallbacks?,
|
||||
request: GetIidTokenRequest?,
|
||||
apiMetadata: ApiMetadata?,
|
||||
) {
|
||||
Log.i(TAG, "getIidToken(): $request")
|
||||
if (cb == null || request == null) return
|
||||
serviceScope.launch {
|
||||
handleGetIidToken(
|
||||
context,
|
||||
ConstellationCallbacksWrapper(cb),
|
||||
request
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPnvCapabilities(
|
||||
cb: IConstellationCallbacks?,
|
||||
request: GetPnvCapabilitiesRequest?,
|
||||
apiMetadata: ApiMetadata?,
|
||||
) {
|
||||
Log.i(TAG, "getPnvCapabilities(): $request")
|
||||
if (cb == null || request == null) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) return
|
||||
serviceScope.launch {
|
||||
handleGetPnvCapabilities(
|
||||
context,
|
||||
ConstellationCallbacksWrapper(cb),
|
||||
request
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import com.google.android.gms.common.api.ApiMetadata
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.constellation.GetIidTokenResponse
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesResponse
|
||||
import com.google.android.gms.constellation.PhoneNumberInfo
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberResponse
|
||||
import com.google.android.gms.constellation.internal.IConstellationCallbacks
|
||||
|
||||
class ConstellationCallbacksWrapper(
|
||||
private val cb: IConstellationCallbacks
|
||||
) : IConstellationCallbacks {
|
||||
|
||||
override fun onPhoneNumberVerified(
|
||||
status: Status?,
|
||||
phoneNumbers: List<PhoneNumberInfo?>?,
|
||||
apiMetadata: ApiMetadata?
|
||||
) = runRemote("onPhoneNumberVerified") {
|
||||
cb.onPhoneNumberVerified(status, phoneNumbers, apiMetadata)
|
||||
}
|
||||
|
||||
override fun onPhoneNumberVerificationsCompleted(
|
||||
status: Status?,
|
||||
response: VerifyPhoneNumberResponse?,
|
||||
apiMetadata: ApiMetadata?
|
||||
) = runRemote("onPhoneNumberVerificationsCompleted") {
|
||||
cb.onPhoneNumberVerificationsCompleted(status, response, apiMetadata)
|
||||
}
|
||||
|
||||
override fun onIidTokenGenerated(
|
||||
status: Status?,
|
||||
response: GetIidTokenResponse?,
|
||||
apiMetadata: ApiMetadata?
|
||||
) = runRemote("onIidTokenGenerated") {
|
||||
cb.onIidTokenGenerated(status, response, apiMetadata)
|
||||
}
|
||||
|
||||
override fun onGetPnvCapabilitiesCompleted(
|
||||
status: Status?,
|
||||
response: GetPnvCapabilitiesResponse?,
|
||||
apiMetadata: ApiMetadata?
|
||||
) = runRemote("onGetPnvCapabilitiesCompleted") {
|
||||
cb.onGetPnvCapabilitiesCompleted(status, response, apiMetadata)
|
||||
}
|
||||
|
||||
override fun asBinder(): IBinder = cb.asBinder()
|
||||
|
||||
private inline fun runRemote(methodName: String, block: () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(TAG, "RemoteException in $methodName", e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ConstellationCallbacks"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
|
||||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import com.squareup.wire.Instant
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.microg.gms.constellation.core.proto.AsterismConsent.DeviceConsentVersion
|
||||
import org.microg.gms.constellation.core.proto.Consent
|
||||
import org.microg.gms.constellation.core.proto.ConsentSource
|
||||
import org.microg.gms.constellation.core.proto.DroidguardToken
|
||||
import org.microg.gms.constellation.core.proto.ProceedResponse
|
||||
import org.microg.gms.constellation.core.proto.ServerTimestamp
|
||||
import org.microg.gms.constellation.core.proto.SyncResponse
|
||||
import org.microg.gms.constellation.core.proto.VerificationToken
|
||||
|
||||
private const val STATE_PREFS_NAME = "constellation_prefs"
|
||||
private const val TOKEN_PREFS_NAME = "com.google.android.gms.constellation"
|
||||
private const val KEY_VERIFICATION_TOKENS = "verification_tokens_v1"
|
||||
private const val KEY_DROIDGUARD_TOKEN = "droidguard_token"
|
||||
private const val KEY_DROIDGUARD_TOKEN_TTL = "droidguard_token_ttl"
|
||||
private const val KEY_NEXT_SYNC_TIMESTAMP_MS = "next_sync_timestamp_in_millis"
|
||||
private const val KEY_PUBLIC_KEY_ACKED = "is_public_key_acked"
|
||||
private const val KEY_PNVR_NOTICE_CONSENT = "pnvr_notice_consent"
|
||||
private const val KEY_PNVR_NOTICE_SOURCE = "pnvr_notice_source"
|
||||
private const val KEY_PNVR_NOTICE_VERSION = "pnvr_notice_version"
|
||||
private const val KEY_PNVR_NOTICE_UPDATED_AT_MS = "pnvr_notice_updated_at_ms"
|
||||
|
||||
object ConstellationStateStore {
|
||||
fun loadVerificationTokens(context: Context): List<VerificationToken> {
|
||||
val prefs = tokenPrefs(context)
|
||||
val serialized = prefs.getString(KEY_VERIFICATION_TOKENS, null) ?: return emptyList()
|
||||
return serialized.split(",").mapNotNull { entry ->
|
||||
val parts = entry.split("|", limit = 2)
|
||||
if (parts.size != 2) return@mapNotNull null
|
||||
val tokenBytes = runCatching {
|
||||
Base64.decode(parts[0], Base64.DEFAULT).toByteString()
|
||||
}.getOrNull() ?: return@mapNotNull null
|
||||
val expirationMillis = parts[1].toLongOrNull() ?: return@mapNotNull null
|
||||
if (expirationMillis <= System.currentTimeMillis()) return@mapNotNull null
|
||||
VerificationToken(
|
||||
token = tokenBytes,
|
||||
expiration_time = Instant.ofEpochMilli(expirationMillis)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun storeSyncResponse(context: Context, response: SyncResponse) {
|
||||
storeVerificationTokens(context, response.verification_tokens)
|
||||
storeDroidGuardToken(context, response.droidguard_token)
|
||||
storeNextSyncTime(context, response.next_sync_time)
|
||||
}
|
||||
|
||||
fun storeProceedResponse(context: Context, response: ProceedResponse) {
|
||||
storeDroidGuardToken(context, response.droidguard_token)
|
||||
storeNextSyncTime(context, response.next_sync_time)
|
||||
}
|
||||
|
||||
fun loadDroidGuardToken(context: Context): String? {
|
||||
val statePrefs = statePrefs(context)
|
||||
var token = statePrefs.getString(KEY_DROIDGUARD_TOKEN, null)
|
||||
var expiration = statePrefs.getLong(KEY_DROIDGUARD_TOKEN_TTL, 0L)
|
||||
|
||||
if (token.isNullOrBlank() || expiration == 0L) {
|
||||
val legacyPrefs = tokenPrefs(context)
|
||||
val legacyToken = legacyPrefs.getString(KEY_DROIDGUARD_TOKEN, null)
|
||||
val legacyExpiration = legacyPrefs.getLong(KEY_DROIDGUARD_TOKEN_TTL, 0L)
|
||||
if (!legacyToken.isNullOrBlank() && legacyExpiration > 0L) {
|
||||
statePrefs.edit {
|
||||
putString(KEY_DROIDGUARD_TOKEN, legacyToken)
|
||||
putLong(KEY_DROIDGUARD_TOKEN_TTL, legacyExpiration)
|
||||
}
|
||||
token = legacyToken
|
||||
expiration = legacyExpiration
|
||||
}
|
||||
}
|
||||
|
||||
if (!token.isNullOrBlank() && expiration > System.currentTimeMillis()) {
|
||||
return token
|
||||
}
|
||||
|
||||
if (!token.isNullOrBlank() || expiration > 0L) {
|
||||
clearDroidGuardToken(context)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun clearDroidGuardToken(context: Context) {
|
||||
statePrefs(context).edit {
|
||||
remove(KEY_DROIDGUARD_TOKEN)
|
||||
remove(KEY_DROIDGUARD_TOKEN_TTL)
|
||||
}
|
||||
tokenPrefs(context).edit {
|
||||
remove(KEY_DROIDGUARD_TOKEN)
|
||||
remove(KEY_DROIDGUARD_TOKEN_TTL)
|
||||
}
|
||||
}
|
||||
|
||||
fun isPublicKeyAcked(context: Context): Boolean {
|
||||
return statePrefs(context).getBoolean(KEY_PUBLIC_KEY_ACKED, false)
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun setPublicKeyAcked(context: Context, acked: Boolean) {
|
||||
statePrefs(context).edit { putBoolean(KEY_PUBLIC_KEY_ACKED, acked) }
|
||||
}
|
||||
|
||||
fun loadPnvrNoticeConsent(context: Context): Consent {
|
||||
val value =
|
||||
statePrefs(context).getInt(KEY_PNVR_NOTICE_CONSENT, Consent.CONSENT_UNKNOWN.value)
|
||||
return Consent.fromValue(value) ?: Consent.CONSENT_UNKNOWN
|
||||
}
|
||||
|
||||
fun storePnvrNotice(
|
||||
context: Context,
|
||||
consent: Consent,
|
||||
source: ConsentSource,
|
||||
version: DeviceConsentVersion
|
||||
) {
|
||||
statePrefs(context).edit {
|
||||
putInt(KEY_PNVR_NOTICE_CONSENT, consent.value)
|
||||
putInt(KEY_PNVR_NOTICE_SOURCE, source.value)
|
||||
putInt(KEY_PNVR_NOTICE_VERSION, version.value)
|
||||
putLong(KEY_PNVR_NOTICE_UPDATED_AT_MS, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeVerificationTokens(context: Context, tokens: List<VerificationToken>) {
|
||||
if (tokens.isEmpty()) return
|
||||
val filtered = tokens.filter {
|
||||
(it.expiration_time?.toEpochMilli() ?: 0L) > System.currentTimeMillis()
|
||||
}
|
||||
if (filtered.isEmpty()) return
|
||||
|
||||
val serialized = filtered.joinToString(",") { token ->
|
||||
val encoded = Base64.encodeToString(token.token.toByteArray(), Base64.NO_WRAP)
|
||||
val expiration = token.expiration_time?.toEpochMilli() ?: 0L
|
||||
"$encoded|$expiration"
|
||||
}
|
||||
tokenPrefs(context).edit { putString(KEY_VERIFICATION_TOKENS, serialized) }
|
||||
}
|
||||
|
||||
private fun storeDroidGuardToken(context: Context, token: DroidguardToken?) {
|
||||
val tokenValue = token?.token?.takeIf { it.isNotEmpty() } ?: return
|
||||
val expiration = token.ttl?.toEpochMilli() ?: return
|
||||
if (tokenValue.isBlank() || expiration <= System.currentTimeMillis()) return
|
||||
statePrefs(context).edit {
|
||||
putString(KEY_DROIDGUARD_TOKEN, tokenValue)
|
||||
putLong(KEY_DROIDGUARD_TOKEN_TTL, expiration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeNextSyncTime(context: Context, timestamp: ServerTimestamp?) {
|
||||
val serverMillis = timestamp?.timestamp?.toEpochMilli() ?: return
|
||||
val localMillis = timestamp.now?.toEpochMilli() ?: return
|
||||
statePrefs(context).edit {
|
||||
// GMS stores the next sync deadline as an absolute wall-clock timestamp
|
||||
putLong(
|
||||
KEY_NEXT_SYNC_TIMESTAMP_MS,
|
||||
System.currentTimeMillis() + serverMillis - localMillis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun statePrefs(context: Context) =
|
||||
context.getSharedPreferences(STATE_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private fun tokenPrefs(context: Context) =
|
||||
context.getSharedPreferences(TOKEN_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
|
||||
// TODO: This is taken from vending-app, can we have a common client for GServices please?
|
||||
|
||||
object GServices {
|
||||
private val CONTENT_URI: Uri = "content://com.google.android.gsf.gservices".toUri()
|
||||
|
||||
fun getString(resolver: ContentResolver, key: String, defaultValue: String?): String? {
|
||||
var result = defaultValue
|
||||
val cursor = resolver.query(CONTENT_URI, null, null, arrayOf(key), null)
|
||||
cursor?.use {
|
||||
if (cursor.moveToNext()) {
|
||||
result = cursor.getString(1)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun getLong(resolver: ContentResolver, key: String, defaultValue: Long): Long {
|
||||
val result = getString(resolver, key, null)
|
||||
if (result != null) {
|
||||
try {
|
||||
return result.toLong()
|
||||
} catch (_: NumberFormatException) {
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.android.gms.common.api.ApiMetadata
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.constellation.GetIidTokenRequest
|
||||
import com.google.android.gms.constellation.GetIidTokenResponse
|
||||
import com.google.android.gms.constellation.internal.IConstellationCallbacks
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val TAG = "GetIidToken"
|
||||
|
||||
suspend fun handleGetIidToken(
|
||||
context: Context,
|
||||
callbacks: IConstellationCallbacks,
|
||||
request: GetIidTokenRequest
|
||||
) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val authManager = context.authManager
|
||||
val iidToken = authManager.getIidToken(request.projectNumber?.toString())
|
||||
val fid = authManager.getFid()
|
||||
val (signature, timestamp) = authManager.signIidTokenCompat(iidToken)
|
||||
|
||||
callbacks.onIidTokenGenerated(
|
||||
Status.SUCCESS,
|
||||
GetIidTokenResponse(iidToken, fid, signature, timestamp),
|
||||
ApiMetadata.DEFAULT
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getIidToken failed", e)
|
||||
callbacks.onIidTokenGenerated(Status.INTERNAL_ERROR, null, ApiMetadata.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.gms.common.api.ApiMetadata
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesRequest
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesResponse
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesResponse.SimCapability
|
||||
import com.google.android.gms.constellation.VerificationStatus
|
||||
import com.google.android.gms.constellation.internal.IConstellationCallbacks
|
||||
import com.google.android.gms.constellation.verificationCapability
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.MessageDigest
|
||||
|
||||
private const val TAG = "GetPnvCapabilities"
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
suspend fun handleGetPnvCapabilities(
|
||||
context: Context,
|
||||
callbacks: IConstellationCallbacks,
|
||||
request: GetPnvCapabilitiesRequest
|
||||
) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val baseTelephonyManager =
|
||||
context.getSystemService<TelephonyManager>()
|
||||
?: throw IllegalStateException("TelephonyManager unavailable")
|
||||
val subscriptionManager =
|
||||
context.getSystemService<SubscriptionManager>()
|
||||
?: throw IllegalStateException("SubscriptionManager unavailable")
|
||||
val simCapabilities = subscriptionManager.activeSubscriptionInfoList
|
||||
.orEmpty()
|
||||
.filter { request.simSlotIndices.isEmpty() || it.simSlotIndex in request.simSlotIndices }
|
||||
.map { info ->
|
||||
val telephonyManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
baseTelephonyManager.createForSubscriptionId(info.subscriptionId)
|
||||
} else {
|
||||
baseTelephonyManager
|
||||
}
|
||||
|
||||
val carrierId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
telephonyManager.simCarrierId
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
// GMS hardcodes public verification method 9 for the Firebase PNV TS43 capability path.
|
||||
val verificationCapabilities = if (9 in request.verificationTypes) {
|
||||
listOf(
|
||||
verificationCapability(
|
||||
9,
|
||||
when {
|
||||
!GetPnvCapabilitiesApiPhenotype.FPNV_ALLOWED_CARRIER_IDS.contains(
|
||||
carrierId
|
||||
) ->
|
||||
VerificationStatus.UNSUPPORTED_CARRIER
|
||||
|
||||
telephonyManager.simState != TelephonyManager.SIM_STATE_READY ->
|
||||
VerificationStatus.UNSUPPORTED_SIM_NOT_READY
|
||||
|
||||
else -> VerificationStatus.SUPPORTED
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// TODO: Reflection should be used to call telephonyManager.getSubscriberId(it.subscriptionId) for SDK < N
|
||||
val subscriberIdDigest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(telephonyManager.subscriberId.orEmpty().toByteArray())
|
||||
val subscriberIdDigestEncoded =
|
||||
Base64.encodeToString(subscriberIdDigest, Base64.NO_WRAP)
|
||||
|
||||
SimCapability(
|
||||
info.simSlotIndex,
|
||||
subscriberIdDigestEncoded,
|
||||
carrierId,
|
||||
// TODO: SDK < N is TelephonyManager.getSimOperatorNameForSubscription
|
||||
telephonyManager.simOperatorName.orEmpty(),
|
||||
verificationCapabilities
|
||||
)
|
||||
}
|
||||
|
||||
callbacks.onGetPnvCapabilitiesCompleted(
|
||||
Status.SUCCESS,
|
||||
GetPnvCapabilitiesResponse(simCapabilities),
|
||||
ApiMetadata.DEFAULT
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "getPnvCapabilities missing permission", e)
|
||||
callbacks.onGetPnvCapabilitiesCompleted(
|
||||
Status(5000),
|
||||
GetPnvCapabilitiesResponse(emptyList()),
|
||||
ApiMetadata.DEFAULT
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getPnvCapabilities failed", e)
|
||||
callbacks.onGetPnvCapabilitiesCompleted(
|
||||
Status.INTERNAL_ERROR,
|
||||
GetPnvCapabilitiesResponse(emptyList()),
|
||||
ApiMetadata.DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
object GetPnvCapabilitiesApiPhenotype {
|
||||
val FPNV_ALLOWED_CARRIER_IDS = ArrayList<Int>()
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
@file:SuppressLint("NewApi")
|
||||
|
||||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.google.android.gms.constellation.PhoneNumberInfo
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.microg.gms.common.Constants
|
||||
import org.microg.gms.constellation.core.proto.GetVerifiedPhoneNumbersRequest
|
||||
import org.microg.gms.constellation.core.proto.GetVerifiedPhoneNumbersRequest.PhoneNumberSelection
|
||||
import org.microg.gms.constellation.core.proto.IIDTokenAuth
|
||||
import org.microg.gms.constellation.core.proto.TokenOption
|
||||
import org.microg.gms.constellation.core.proto.VerifiedPhoneNumber
|
||||
import java.util.UUID
|
||||
|
||||
private const val TAG = "GetVerifiedPhoneNumbers"
|
||||
|
||||
internal suspend fun fetchVerifiedPhoneNumbers(
|
||||
context: Context,
|
||||
bundle: Bundle,
|
||||
callingPackage: String = bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME
|
||||
): List<VerifiedPhoneNumber> = withContext(Dispatchers.IO) {
|
||||
val authManager = context.authManager
|
||||
val sessionId = UUID.randomUUID().toString()
|
||||
val selections = extractPhoneNumberSelections(bundle)
|
||||
val certificateHash = bundle.getString("certificate_hash") ?: ""
|
||||
val tokenNonce = bundle.getString("token_nonce") ?: ""
|
||||
|
||||
val iidToken = authManager.getIidToken(IidTokenPhenotypes.READ_ONLY_PROJECT_NUMBER)
|
||||
val iidTokenAuth = if (VerifyPhoneNumberApiPhenotypes.ENABLE_CLIENT_SIGNATURE) {
|
||||
val (signatureBytes, signTimestamp) = authManager.signIidToken(iidToken)
|
||||
IIDTokenAuth(
|
||||
iid_token = iidToken,
|
||||
client_sign = signatureBytes.toByteString(),
|
||||
sign_timestamp = signTimestamp
|
||||
)
|
||||
} else {
|
||||
IIDTokenAuth(iid_token = iidToken)
|
||||
}
|
||||
|
||||
val getRequest = GetVerifiedPhoneNumbersRequest(
|
||||
session_id = sessionId,
|
||||
iid_token_auth = iidTokenAuth,
|
||||
phone_number_selections = selections,
|
||||
token_option = TokenOption(
|
||||
certificate_hash = certificateHash,
|
||||
token_nonce = tokenNonce,
|
||||
package_name = callingPackage
|
||||
)
|
||||
)
|
||||
|
||||
Log.d(TAG, "Calling GetVerifiedPhoneNumbers RPC (read-only mode)...")
|
||||
val response = RpcClient.phoneNumberClient
|
||||
.GetVerifiedPhoneNumbers()
|
||||
.execute(getRequest)
|
||||
Log.d(TAG, "GetVerifiedPhoneNumbers response: ${response.phone_numbers.size} numbers")
|
||||
response.phone_numbers
|
||||
}
|
||||
|
||||
internal fun VerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo {
|
||||
val extras = Bundle().apply {
|
||||
if (id_token.isNotEmpty()) {
|
||||
putString("id_token", id_token)
|
||||
}
|
||||
putInt("rcs_state", rcs_state.value)
|
||||
}
|
||||
|
||||
return PhoneNumberInfo(
|
||||
1,
|
||||
phone_number,
|
||||
verification_time?.toEpochMilli() ?: 0L,
|
||||
extras
|
||||
)
|
||||
}
|
||||
|
||||
internal fun VerifiedPhoneNumber.toPhoneNumberVerification(): PhoneNumberVerification {
|
||||
val extras = Bundle().apply {
|
||||
putInt("rcs_state", rcs_state.value)
|
||||
}
|
||||
|
||||
// GMS read-only V2 leaves method/slot unset and returns a verified record directly.
|
||||
return PhoneNumberVerification(
|
||||
phone_number,
|
||||
verification_time?.toEpochMilli() ?: 0L,
|
||||
0,
|
||||
-1,
|
||||
id_token.ifEmpty { null },
|
||||
extras,
|
||||
1,
|
||||
-1L
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractPhoneNumberSelections(bundle: Bundle): List<PhoneNumberSelection> {
|
||||
val selections = mutableListOf<PhoneNumberSelection>()
|
||||
val selectionInts = bundle.getIntegerArrayList("phone_number_selection")
|
||||
|
||||
if (!selectionInts.isNullOrEmpty()) {
|
||||
selections.addAll(selectionInts.mapNotNull { PhoneNumberSelection.fromValue(it) })
|
||||
} else {
|
||||
when (bundle.getString("rcs_read_option", "")) {
|
||||
"READ_PROVISIONED" -> {
|
||||
selections.add(PhoneNumberSelection.CONSTELLATION)
|
||||
selections.add(PhoneNumberSelection.RCS)
|
||||
}
|
||||
|
||||
"READ_PROVISIONED_ONLY" -> selections.add(PhoneNumberSelection.RCS)
|
||||
else -> selections.add(PhoneNumberSelection.CONSTELLATION)
|
||||
}
|
||||
}
|
||||
return selections
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
object IidTokenPhenotypes {
|
||||
const val ASTERISM_PROJECT_NUMBER = "496232013492"
|
||||
const val DEFAULT_PROJECT_NUMBER = "496232013492"
|
||||
const val EXTERNAL_CONSENT_ACTIVITY_PROJECT_NUMBER = "496232013492"
|
||||
const val MESSAGES_PROJECT_NUMBER = "496232013492"
|
||||
const val READ_ONLY_PROJECT_NUMBER = "745476177629"
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import com.squareup.wire.GrpcClient
|
||||
import okhttp3.OkHttpClient
|
||||
import org.microg.gms.common.Constants
|
||||
import org.microg.gms.constellation.core.proto.PhoneDeviceVerificationClient
|
||||
import org.microg.gms.constellation.core.proto.PhoneNumberClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object RpcClient {
|
||||
private val client: OkHttpClient = OkHttpClient.Builder()
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.addInterceptor { chain ->
|
||||
val originalRequest = chain.request()
|
||||
val builder = originalRequest.newBuilder()
|
||||
.header("X-Goog-Api-Key", "AIzaSyAP-gfH3qvi6vgHZbSYwQ_XHqV_mXHhzIk")
|
||||
.header("X-Android-Package", Constants.GMS_PACKAGE_NAME)
|
||||
.header("X-Android-Cert", Constants.GMS_PACKAGE_SIGNATURE_SHA1.uppercase())
|
||||
chain.proceed(builder.build())
|
||||
}
|
||||
.build()
|
||||
|
||||
private val grpcClient: GrpcClient = GrpcClient.Builder()
|
||||
.client(client)
|
||||
// Google's constellationserver does NOT like compressed requests
|
||||
.minMessageToCompress(Long.MAX_VALUE)
|
||||
.baseUrl("https://phonedeviceverification-pa.googleapis.com/")
|
||||
.build()
|
||||
|
||||
val phoneDeviceVerificationClient: PhoneDeviceVerificationClient =
|
||||
grpcClient.create<PhoneDeviceVerificationClient>()
|
||||
|
||||
val phoneNumberClient: PhoneNumberClient =
|
||||
grpcClient.create<PhoneNumberClient>()
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification
|
||||
import org.microg.gms.constellation.core.proto.Param
|
||||
import org.microg.gms.constellation.core.proto.UnverifiedInfo
|
||||
import org.microg.gms.constellation.core.proto.Verification
|
||||
import org.microg.gms.constellation.core.proto.VerificationMethod
|
||||
|
||||
fun UnverifiedInfo.Reason.toVerificationStatus(): Verification.Status {
|
||||
return when (this) {
|
||||
UnverifiedInfo.Reason.UNKNOWN_REASON -> Verification.Status.STATUS_UNKNOWN
|
||||
UnverifiedInfo.Reason.THROTTLED -> Verification.Status.STATUS_THROTTLED
|
||||
UnverifiedInfo.Reason.FAILED -> Verification.Status.STATUS_FAILED
|
||||
UnverifiedInfo.Reason.SKIPPED -> Verification.Status.STATUS_SKIPPED
|
||||
UnverifiedInfo.Reason.NOT_REQUIRED -> Verification.Status.STATUS_NOT_REQUIRED
|
||||
UnverifiedInfo.Reason.PHONE_NUMBER_ENTRY_REQUIRED ->
|
||||
Verification.Status.STATUS_PHONE_NUMBER_ENTRY_REQUIRED
|
||||
|
||||
UnverifiedInfo.Reason.INELIGIBLE -> Verification.Status.STATUS_INELIGIBLE
|
||||
UnverifiedInfo.Reason.DENIED -> Verification.Status.STATUS_DENIED
|
||||
UnverifiedInfo.Reason.NOT_IN_SERVICE -> Verification.Status.STATUS_NOT_IN_SERVICE
|
||||
}
|
||||
}
|
||||
|
||||
val Verification.state: Verification.State
|
||||
get() = when {
|
||||
verification_info != null -> Verification.State.VERIFIED
|
||||
pending_verification_info != null -> Verification.State.PENDING
|
||||
unverified_info != null -> Verification.State.NONE
|
||||
else -> Verification.State.UNKNOWN
|
||||
}
|
||||
|
||||
val Verification.effectiveStatus: Verification.Status
|
||||
get() = when (state) {
|
||||
Verification.State.VERIFIED -> Verification.Status.STATUS_VERIFIED
|
||||
Verification.State.PENDING ->
|
||||
if (status != Verification.Status.STATUS_UNKNOWN) {
|
||||
status
|
||||
} else {
|
||||
Verification.Status.STATUS_PENDING
|
||||
}
|
||||
|
||||
Verification.State.NONE ->
|
||||
unverified_info?.reason?.toVerificationStatus() ?: Verification.Status.STATUS_UNKNOWN
|
||||
|
||||
Verification.State.UNKNOWN -> Verification.Status.STATUS_UNKNOWN
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun Verification.toClientVerification(imsiToSlotMap: Map<String, Int>): PhoneNumberVerification {
|
||||
val clientStatus = effectiveStatus.toClientStatus()
|
||||
val extras = buildClientExtras()
|
||||
val simImsi = association?.sim?.sim_info?.imsi?.firstOrNull()
|
||||
val simSlot = if (simImsi != null) imsiToSlotMap[simImsi] ?: -1 else -1
|
||||
|
||||
var phoneNumber = ""
|
||||
var timestampMillis = 0L
|
||||
var verificationMethod = VerificationMethod.UNKNOWN
|
||||
var verificationToken: String? = null
|
||||
var retryAfterSeconds = -1L
|
||||
|
||||
when (state) {
|
||||
Verification.State.VERIFIED -> {
|
||||
val info = verification_info
|
||||
val verifiedPhoneNumber = info?.phone_number
|
||||
require(!verifiedPhoneNumber.isNullOrEmpty()) { "Verified phone number is empty" }
|
||||
phoneNumber = verifiedPhoneNumber
|
||||
timestampMillis = info.verification_time?.toEpochMilli() ?: 0L
|
||||
verificationMethod = info.challenge_method
|
||||
verificationToken = extras.getString("id_token")
|
||||
extras.remove("phone_number")
|
||||
extras.remove("id_token")
|
||||
extras.remove("verification_time_millis")
|
||||
}
|
||||
|
||||
Verification.State.PENDING -> {
|
||||
verificationMethod =
|
||||
pending_verification_info?.challenge?.type ?: VerificationMethod.UNKNOWN
|
||||
}
|
||||
|
||||
Verification.State.NONE -> {
|
||||
val info = unverified_info
|
||||
verificationMethod = info?.challenge_method ?: VerificationMethod.UNKNOWN
|
||||
retryAfterSeconds = info?.retry_after_time?.let { ts ->
|
||||
val now = System.currentTimeMillis() / 1000L
|
||||
(ts.epochSecond - now).coerceAtLeast(0L)
|
||||
} ?: -1L
|
||||
}
|
||||
|
||||
Verification.State.UNKNOWN -> Unit
|
||||
}
|
||||
|
||||
extras.remove("verification_method")
|
||||
extras.remove("sim_slot_index")
|
||||
|
||||
return PhoneNumberVerification(
|
||||
phoneNumber,
|
||||
timestampMillis,
|
||||
verificationMethod.toClientMethod(),
|
||||
simSlot,
|
||||
verificationToken,
|
||||
extras,
|
||||
clientStatus,
|
||||
retryAfterSeconds
|
||||
)
|
||||
}
|
||||
|
||||
private fun Verification.buildClientExtras(): Bundle {
|
||||
val bundle = Bundle()
|
||||
for (param in api_params) {
|
||||
bundle.putParam(param)
|
||||
}
|
||||
|
||||
val slotIndex = association?.sim?.sim_slot?.slot_index
|
||||
if (slotIndex != null && slotIndex >= 0) {
|
||||
bundle.putString("sim_slot_index", slotIndex.toString())
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
|
||||
private fun Bundle.putParam(param: Param) {
|
||||
if (param.key == "verification_method") {
|
||||
param.value_.toIntOrNull()?.let {
|
||||
putInt(param.key, it)
|
||||
return
|
||||
}
|
||||
}
|
||||
putString(param.key, param.value_)
|
||||
}
|
||||
|
||||
fun VerificationMethod.toClientMethod(): Int {
|
||||
return when (this) {
|
||||
VerificationMethod.UNKNOWN -> 0
|
||||
VerificationMethod.TS43 -> 9
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
|
||||
fun Verification.Status.toClientStatus(): Int {
|
||||
return when (this) {
|
||||
Verification.Status.STATUS_UNKNOWN,
|
||||
Verification.Status.STATUS_NONE -> 0
|
||||
|
||||
Verification.Status.STATUS_PENDING -> 6
|
||||
Verification.Status.STATUS_VERIFIED -> 1
|
||||
Verification.Status.STATUS_THROTTLED -> 3
|
||||
Verification.Status.STATUS_FAILED -> 2
|
||||
Verification.Status.STATUS_SKIPPED -> 4
|
||||
Verification.Status.STATUS_NOT_REQUIRED -> 5
|
||||
Verification.Status.STATUS_PHONE_NUMBER_ENTRY_REQUIRED -> 7
|
||||
Verification.Status.STATUS_INELIGIBLE -> 8
|
||||
Verification.Status.STATUS_DENIED -> 9
|
||||
Verification.Status.STATUS_NOT_IN_SERVICE -> 10
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
object VerificationSettingsPhenotypes {
|
||||
const val A2P_SMS_SIGNAL_GRANULARITY_HRS = 1L
|
||||
const val A2P_HISTORY_WINDOW_HOURS = 168L
|
||||
}
|
||||
|
|
@ -0,0 +1,459 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
|
||||
package org.microg.gms.constellation.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.gms.common.api.ApiMetadata
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.constellation.PhoneNumberInfo
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberRequest
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberRequest.IdTokenRequest
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberResponse
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification
|
||||
import com.google.android.gms.constellation.internal.IConstellationCallbacks
|
||||
import com.squareup.wire.GrpcException
|
||||
import com.squareup.wire.GrpcStatus
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.common.Constants
|
||||
import org.microg.gms.constellation.core.proto.AsterismClient
|
||||
import org.microg.gms.constellation.core.proto.Consent
|
||||
import org.microg.gms.constellation.core.proto.ConsentVersion
|
||||
import org.microg.gms.constellation.core.proto.DeviceID
|
||||
import org.microg.gms.constellation.core.proto.GetConsentRequest
|
||||
import org.microg.gms.constellation.core.proto.GetConsentResponse
|
||||
import org.microg.gms.constellation.core.proto.Param
|
||||
import org.microg.gms.constellation.core.proto.RcsConsent
|
||||
import org.microg.gms.constellation.core.proto.RequestHeader
|
||||
import org.microg.gms.constellation.core.proto.SetConsentRequest
|
||||
import org.microg.gms.constellation.core.proto.SyncRequest
|
||||
import org.microg.gms.constellation.core.proto.Verification
|
||||
import org.microg.gms.constellation.core.proto.builder.RequestBuildContext
|
||||
import org.microg.gms.constellation.core.proto.builder.buildImsiToSubscriptionInfoMap
|
||||
import org.microg.gms.constellation.core.proto.builder.buildRequestContext
|
||||
import org.microg.gms.constellation.core.proto.builder.getList
|
||||
import org.microg.gms.constellation.core.proto.builder.invoke
|
||||
import org.microg.gms.constellation.core.verification.ChallengeProcessor
|
||||
import org.microg.gms.constellation.core.verification.MtSmsInboxRegistry
|
||||
import java.util.UUID
|
||||
|
||||
private const val TAG = "VerifyPhoneNumber"
|
||||
|
||||
private enum class ReadCallbackMode {
|
||||
NONE,
|
||||
LEGACY,
|
||||
TYPED
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun handleVerifyPhoneNumberV1(
|
||||
context: Context,
|
||||
callbacks: IConstellationCallbacks,
|
||||
bundle: Bundle,
|
||||
packageName: String?
|
||||
) {
|
||||
val callingPackage =
|
||||
packageName ?: bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME
|
||||
val extras = Bundle(bundle).apply {
|
||||
putString("calling_package", callingPackage)
|
||||
putString("calling_api", "verifyPhoneNumber")
|
||||
}
|
||||
val timeout = when (val timeoutValue = extras.get("timeout")) {
|
||||
is Long -> timeoutValue
|
||||
is Int -> timeoutValue.toLong()
|
||||
else -> 300L
|
||||
}
|
||||
val request = VerifyPhoneNumberRequest(
|
||||
extras.getString("policy_id", ""),
|
||||
timeout,
|
||||
IdTokenRequest(
|
||||
extras.getString("certificate_hash", ""),
|
||||
extras.getString("token_nonce", "")
|
||||
),
|
||||
extras,
|
||||
emptyList(),
|
||||
false,
|
||||
2,
|
||||
emptyList()
|
||||
)
|
||||
val policyId = bundle.getString("policy_id", "")
|
||||
val mode = bundle.getInt("verification_mode", 0)
|
||||
val useReadPath = when (mode) {
|
||||
0 -> policyId in VerifyPhoneNumberApiPhenotypes.READ_ONLY_POLICY_IDS
|
||||
2 -> VerifyPhoneNumberApiPhenotypes.ENABLE_READ_FLOW
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
handleVerifyPhoneNumberRequest(
|
||||
context,
|
||||
callbacks,
|
||||
request,
|
||||
callingPackage,
|
||||
if (useReadPath) ReadCallbackMode.LEGACY else ReadCallbackMode.NONE,
|
||||
legacyCallbackOnFullFlow = true
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun handleVerifyPhoneNumberSingleUse(
|
||||
context: Context,
|
||||
callbacks: IConstellationCallbacks,
|
||||
bundle: Bundle,
|
||||
packageName: String?
|
||||
) {
|
||||
val callingPackage =
|
||||
packageName ?: bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME
|
||||
val extras = Bundle(bundle).apply {
|
||||
putString("calling_package", callingPackage)
|
||||
putString("calling_api", "verifyPhoneNumberSingleUse")
|
||||
putString("one_time_verification", "True")
|
||||
}
|
||||
val timeout = when (val timeoutValue = extras.get("timeout")) {
|
||||
is Long -> timeoutValue
|
||||
is Int -> timeoutValue.toLong()
|
||||
else -> 300L
|
||||
}
|
||||
val request = VerifyPhoneNumberRequest(
|
||||
extras.getString("policy_id", ""),
|
||||
timeout,
|
||||
IdTokenRequest(
|
||||
extras.getString("certificate_hash", ""),
|
||||
extras.getString("token_nonce", "")
|
||||
),
|
||||
extras,
|
||||
emptyList(),
|
||||
false,
|
||||
2,
|
||||
emptyList()
|
||||
)
|
||||
|
||||
handleVerifyPhoneNumberRequest(
|
||||
context,
|
||||
callbacks,
|
||||
request,
|
||||
callingPackage,
|
||||
ReadCallbackMode.NONE,
|
||||
legacyCallbackOnFullFlow = true
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun handleVerifyPhoneNumberRequest(
|
||||
context: Context,
|
||||
callbacks: IConstellationCallbacks,
|
||||
request: VerifyPhoneNumberRequest,
|
||||
packageName: String?
|
||||
) {
|
||||
val callingPackage = packageName ?: Constants.GMS_PACKAGE_NAME
|
||||
request.extras.putString("calling_api", "verifyPhoneNumber")
|
||||
val useReadPath = when (request.apiVersion) {
|
||||
0 -> request.policyId in VerifyPhoneNumberApiPhenotypes.READ_ONLY_POLICY_IDS
|
||||
2 -> VerifyPhoneNumberApiPhenotypes.ENABLE_READ_FLOW
|
||||
3 -> request.policyId in VerifyPhoneNumberApiPhenotypes.POLICY_IDS_ALLOWED_FOR_LOCAL_READ
|
||||
else -> false
|
||||
}
|
||||
|
||||
handleVerifyPhoneNumberRequest(
|
||||
context,
|
||||
callbacks,
|
||||
request,
|
||||
callingPackage,
|
||||
if (useReadPath) ReadCallbackMode.TYPED else ReadCallbackMode.NONE,
|
||||
localReadFallback = request.apiVersion == 3 && useReadPath,
|
||||
legacyCallbackOnFullFlow = false
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleVerifyPhoneNumberRequest(
|
||||
context: Context,
|
||||
callbacks: IConstellationCallbacks,
|
||||
request: VerifyPhoneNumberRequest,
|
||||
callingPackage: String,
|
||||
readCallbackMode: ReadCallbackMode,
|
||||
localReadFallback: Boolean = false,
|
||||
legacyCallbackOnFullFlow: Boolean = false
|
||||
) {
|
||||
var phoneNumbers = emptyList<PhoneNumberInfo>()
|
||||
var verifications = emptyArray<PhoneNumberVerification>()
|
||||
val status = try {
|
||||
when (readCallbackMode) {
|
||||
ReadCallbackMode.LEGACY -> {
|
||||
Log.d(TAG, "Using read-only mode")
|
||||
phoneNumbers = fetchVerifiedPhoneNumbers(context, request.extras)
|
||||
.map { it.toPhoneNumberInfo() }
|
||||
Status.SUCCESS
|
||||
}
|
||||
|
||||
ReadCallbackMode.TYPED -> {
|
||||
if (localReadFallback) {
|
||||
Log.w(TAG, "Local-read mode not implemented, falling back to read-only RPC")
|
||||
} else {
|
||||
Log.d(TAG, "Using typed read-only mode")
|
||||
}
|
||||
verifications = fetchVerifiedPhoneNumbers(context, request.extras, callingPackage)
|
||||
.map { it.toPhoneNumberVerification() }
|
||||
.toTypedArray()
|
||||
Status.SUCCESS
|
||||
}
|
||||
|
||||
ReadCallbackMode.NONE -> {
|
||||
Log.d(TAG, "Using full verification mode")
|
||||
verifications = runVerificationFlow(context, request, callingPackage)
|
||||
Status.SUCCESS
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "verifyPhoneNumber failed", e)
|
||||
when {
|
||||
readCallbackMode != ReadCallbackMode.NONE -> Status.INTERNAL_ERROR
|
||||
e is GrpcException -> handleRpcError(e)
|
||||
e is NoConsentException -> Status(5001)
|
||||
else -> Status.INTERNAL_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
if (readCallbackMode == ReadCallbackMode.LEGACY ||
|
||||
readCallbackMode == ReadCallbackMode.NONE && legacyCallbackOnFullFlow
|
||||
) {
|
||||
if (readCallbackMode != ReadCallbackMode.LEGACY) {
|
||||
phoneNumbers = verifications.mapNotNull { it.toLegacyPhoneNumberInfoOrNull() }
|
||||
}
|
||||
callbacks.onPhoneNumberVerified(status, phoneNumbers, ApiMetadata.DEFAULT)
|
||||
return
|
||||
}
|
||||
|
||||
callbacks.onPhoneNumberVerificationsCompleted(
|
||||
status,
|
||||
VerifyPhoneNumberResponse(
|
||||
verifications,
|
||||
Bundle()
|
||||
),
|
||||
ApiMetadata.DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleRpcError(error: GrpcException): Status {
|
||||
val statusCode = when (error.grpcStatus) {
|
||||
GrpcStatus.RESOURCE_EXHAUSTED -> 5008
|
||||
GrpcStatus.DEADLINE_EXCEEDED,
|
||||
GrpcStatus.ABORTED,
|
||||
GrpcStatus.UNAVAILABLE -> 5007
|
||||
|
||||
GrpcStatus.PERMISSION_DENIED -> 5009
|
||||
else -> 5002
|
||||
}
|
||||
return Status(statusCode, error.message)
|
||||
}
|
||||
|
||||
private class NoConsentException : Exception("No consent")
|
||||
|
||||
private suspend fun runVerificationFlow(
|
||||
context: Context,
|
||||
request: VerifyPhoneNumberRequest,
|
||||
callingPackage: String
|
||||
): Array<PhoneNumberVerification> {
|
||||
val sessionId = UUID.randomUUID().toString()
|
||||
|
||||
val imsiToInfoMap = buildImsiToSubscriptionInfoMap(context)
|
||||
val buildContext = buildRequestContext(context)
|
||||
|
||||
val asterismClient = when (request.extras.getString("required_consumer_consent")) {
|
||||
"RCS" -> AsterismClient.RCS
|
||||
else -> AsterismClient.UNKNOWN_CLIENT
|
||||
}
|
||||
if (
|
||||
request.extras.getString("one_time_verification") != "True" &&
|
||||
asterismClient != AsterismClient.UNKNOWN_CLIENT
|
||||
) {
|
||||
val consent = getConsent(
|
||||
context,
|
||||
buildContext,
|
||||
asterismClient,
|
||||
sessionId,
|
||||
)
|
||||
|
||||
val consented = consent.rcs_consent?.consent == Consent.CONSENTED ||
|
||||
consent.gaia_consents.any {
|
||||
it.asterism_client == asterismClient && it.consent == Consent.CONSENTED
|
||||
}
|
||||
|
||||
if (!consented) {
|
||||
Log.e(TAG, "Consent has not been set. Auto-setting consent.")
|
||||
val consentType = ConsentVersion.fromValue(
|
||||
ConsentVersion.valueOf(
|
||||
request.extras.getString("consent_type") ?: "CONSENT_VERSION_UNSPECIFIED"
|
||||
).value
|
||||
) ?: ConsentVersion.RCS_DEFAULT_ON_LEGAL_FYI
|
||||
val setRequest = SetConsentRequest(
|
||||
header_ = RequestHeader(context, sessionId, buildContext, "setConsent"),
|
||||
asterism_client = asterismClient,
|
||||
rcs_consent = RcsConsent(
|
||||
consent = Consent.CONSENTED,
|
||||
consent_version = consentType
|
||||
),
|
||||
consent_version = consentType,
|
||||
api_params = Param.getList(request.extras)
|
||||
)
|
||||
try {
|
||||
RpcClient.phoneDeviceVerificationClient.SetConsent().execute(setRequest)
|
||||
Log.i(TAG, "Auto-consented for $asterismClient")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Auto-consent failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val syncRequest = SyncRequest(
|
||||
context,
|
||||
sessionId,
|
||||
request,
|
||||
buildContext,
|
||||
imsiToInfoMap = imsiToInfoMap,
|
||||
includeClientAuth = ConstellationStateStore.isPublicKeyAcked(context),
|
||||
callingPackage = callingPackage
|
||||
)
|
||||
|
||||
MtSmsInboxRegistry.prepare(
|
||||
context.applicationContext,
|
||||
imsiToInfoMap.values.map { it.subscriptionId })
|
||||
|
||||
val verifications = try {
|
||||
executeSyncFlow(
|
||||
context,
|
||||
sessionId,
|
||||
request,
|
||||
syncRequest,
|
||||
buildContext,
|
||||
imsiToInfoMap
|
||||
)
|
||||
} finally {
|
||||
MtSmsInboxRegistry.dispose()
|
||||
}
|
||||
|
||||
return verifications
|
||||
}
|
||||
|
||||
private fun PhoneNumberVerification.toLegacyPhoneNumberInfoOrNull(): PhoneNumberInfo? {
|
||||
if (
|
||||
verificationStatus != Verification.Status.STATUS_VERIFIED.toClientStatus() ||
|
||||
phoneNumber.isNullOrEmpty()
|
||||
) {
|
||||
return null
|
||||
}
|
||||
val extras = Bundle(this.extras ?: Bundle.EMPTY).apply {
|
||||
verificationToken?.let { putString("id_token", it) }
|
||||
}
|
||||
return PhoneNumberInfo(1, phoneNumber, timestampMillis, extras)
|
||||
}
|
||||
|
||||
private suspend fun executeSyncFlow(
|
||||
context: Context,
|
||||
sessionId: String,
|
||||
request: VerifyPhoneNumberRequest,
|
||||
syncRequest: SyncRequest,
|
||||
buildContext: RequestBuildContext,
|
||||
imsiToInfoMap: Map<String, SubscriptionInfo>
|
||||
): Array<PhoneNumberVerification> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Sending Sync request")
|
||||
val syncResponse = try {
|
||||
RpcClient.phoneDeviceVerificationClient.Sync().execute(syncRequest)
|
||||
} catch (e: GrpcException) {
|
||||
if (e.grpcStatus == GrpcStatus.PERMISSION_DENIED ||
|
||||
e.grpcStatus == GrpcStatus.UNAUTHENTICATED
|
||||
) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache..."
|
||||
)
|
||||
ConstellationStateStore.clearDroidGuardToken(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
Log.d(TAG, "Sync response: ${syncResponse.responses.size} verifications")
|
||||
ConstellationStateStore.storeSyncResponse(context, syncResponse)
|
||||
|
||||
val isPublicKeyAcked = syncResponse.header_?.status?.code == 1
|
||||
val imsiToSlotMap = imsiToInfoMap.mapValues { it.value.simSlotIndex }
|
||||
val requestedImsis = request.targetedSims.map { it.imsi }.toSet()
|
||||
|
||||
val verifications = syncResponse.responses.mapNotNull { result ->
|
||||
val verification = result.verification ?: Verification()
|
||||
val verificationImsis = verification.association?.sim?.sim_info?.imsi.orEmpty()
|
||||
if (requestedImsis.isNotEmpty() && verificationImsis.none { it in requestedImsis }) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Skipping verification for IMSIs=$verificationImsis because it does not match requested IMSIs=$requestedImsis"
|
||||
)
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val finalVerification = if (verification.state == Verification.State.PENDING) {
|
||||
ChallengeProcessor.process(
|
||||
context,
|
||||
sessionId,
|
||||
imsiToInfoMap,
|
||||
buildContext,
|
||||
verification
|
||||
)
|
||||
} else {
|
||||
verification
|
||||
}
|
||||
|
||||
if (finalVerification.state != Verification.State.VERIFIED) {
|
||||
Log.w(TAG, "Unverified. State: ${finalVerification.state}")
|
||||
(finalVerification.pending_verification_info
|
||||
?: finalVerification.unverified_info)?.let { Log.w(TAG, it.toString()) }
|
||||
|
||||
if (!request.includeUnverified) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
|
||||
finalVerification.toClientVerification(imsiToSlotMap)
|
||||
}.toTypedArray()
|
||||
|
||||
if (isPublicKeyAcked) {
|
||||
Log.d(TAG, "Server acknowledged client public key")
|
||||
ConstellationStateStore.setPublicKeyAcked(context, true)
|
||||
}
|
||||
|
||||
verifications
|
||||
}
|
||||
|
||||
suspend fun getConsent(
|
||||
context: Context,
|
||||
buildContext: RequestBuildContext,
|
||||
asterismClient: AsterismClient,
|
||||
sessionId: String = UUID.randomUUID().toString()
|
||||
): GetConsentResponse {
|
||||
val request = GetConsentRequest(
|
||||
device_id = DeviceID(context, buildContext.iidToken),
|
||||
header_ = RequestHeader(
|
||||
context,
|
||||
sessionId,
|
||||
buildContext,
|
||||
"getConsent"
|
||||
),
|
||||
asterism_client = asterismClient
|
||||
)
|
||||
|
||||
return try {
|
||||
RpcClient.phoneDeviceVerificationClient.GetConsent().execute(request)
|
||||
} catch (e: GrpcException) {
|
||||
if (e.grpcStatus == GrpcStatus.PERMISSION_DENIED ||
|
||||
e.grpcStatus == GrpcStatus.UNAUTHENTICATED
|
||||
) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache..."
|
||||
)
|
||||
ConstellationStateStore.clearDroidGuardToken(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package org.microg.gms.constellation.core
|
||||
|
||||
object VerifyPhoneNumberApiPhenotypes {
|
||||
val PACKAGES_ALLOWED_TO_CALL = listOf(
|
||||
"com.google.android.gms",
|
||||
"com.google.android.apps.messaging",
|
||||
"com.google.android.ims",
|
||||
"com.google.android.apps.tachyon",
|
||||
"com.google.android.dialer",
|
||||
"com.google.android.apps.nbu.paisa.user.dev",
|
||||
"com.google.android.apps.nbu.paisa.user.qa",
|
||||
"com.google.android.apps.nbu.paisa.user.teamfood2",
|
||||
"com.google.android.apps.nbu.paisa.user.partner",
|
||||
"com.google.android.apps.nbu.paisa.user",
|
||||
"com.google.android.gms.constellation.getiidtoken",
|
||||
"com.google.android.gms.constellation.ondemandconsent",
|
||||
"com.google.android.gms.constellation.ondemandconsentv2",
|
||||
"com.google.android.gms.constellation.readphonenumber",
|
||||
"com.google.android.gms.constellation.verifyphonenumberlite",
|
||||
"com.google.android.gms.constellation.verifyphonenumber",
|
||||
"com.google.android.gms.test",
|
||||
"com.google.android.apps.stargate",
|
||||
"com.google.android.gms.firebase.fpnv",
|
||||
"com.google.firebase.pnv.testapp",
|
||||
"com.google.firebase.pnv"
|
||||
)
|
||||
val POLICY_IDS_ALLOWED_FOR_LOCAL_READ = listOf("emergency_location")
|
||||
val READ_ONLY_POLICY_IDS = listOf(
|
||||
"business_voice",
|
||||
"verifiedsmsconsent",
|
||||
"hint",
|
||||
"nearbysharing"
|
||||
)
|
||||
const val ENABLE_CLIENT_SIGNATURE = true
|
||||
const val ENABLE_LOCAL_READ_FLOW = true
|
||||
const val ENABLE_READ_FLOW = true
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
|
||||
package org.microg.gms.constellation.core.proto.builder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import android.os.UserManager
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.gms.droidguard.DroidGuard
|
||||
import com.google.android.gms.tasks.await
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.microg.gms.common.Constants
|
||||
import org.microg.gms.constellation.core.ConstellationStateStore
|
||||
import org.microg.gms.constellation.core.GServices
|
||||
import org.microg.gms.constellation.core.authManager
|
||||
import org.microg.gms.constellation.core.proto.ClientInfo
|
||||
import org.microg.gms.constellation.core.proto.CountryInfo
|
||||
import org.microg.gms.constellation.core.proto.DeviceID
|
||||
import org.microg.gms.constellation.core.proto.DeviceType
|
||||
import org.microg.gms.constellation.core.proto.DroidGuardSignals
|
||||
import org.microg.gms.constellation.core.proto.GaiaSignals
|
||||
import org.microg.gms.constellation.core.proto.NetworkSignal
|
||||
import org.microg.gms.constellation.core.proto.SimOperatorInfo
|
||||
import org.microg.gms.constellation.core.proto.UserProfileType
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
private const val TAG = "ClientInfoBuilder"
|
||||
private const val PREFS_NAME = "constellation_prefs"
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
suspend operator fun ClientInfo.Companion.invoke(
|
||||
context: Context,
|
||||
buildContext: RequestBuildContext,
|
||||
rpc: String
|
||||
): ClientInfo {
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
val locale = Locale.getDefault().let { "${it.language}_${it.country}" }
|
||||
|
||||
return ClientInfo(
|
||||
device_id = DeviceID(context, buildContext.iidToken),
|
||||
client_public_key = context.authManager.getOrCreateKeyPair().public.encoded.toByteString(),
|
||||
locale = locale,
|
||||
gmscore_version_number = Constants.GMS_VERSION_CODE / 1000,
|
||||
gmscore_version = packageInfo.versionName ?: "",
|
||||
android_sdk_version = Build.VERSION.SDK_INT,
|
||||
user_profile_type = UserProfileType.REGULAR_USER,
|
||||
gaia_tokens = buildContext.gaiaTokens,
|
||||
country_info = CountryInfo(context),
|
||||
connectivity_infos = NetworkSignal.getList(context),
|
||||
model = Build.MODEL,
|
||||
manufacturer = Build.MANUFACTURER,
|
||||
partial_sim_infos = SimOperatorInfo.getList(context),
|
||||
device_type = DeviceType.DEVICE_TYPE_PHONE,
|
||||
is_wearable_standalone = false,
|
||||
gaia_signals = GaiaSignals(context),
|
||||
device_fingerprint = Build.FINGERPRINT,
|
||||
droidguard_signals = DroidGuardSignals(context, buildContext.iidToken, rpc),
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
operator fun DeviceID.Companion.invoke(context: Context, iidToken: String): DeviceID {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
var gmsAndroidId = prefs.getLong("gms_android_id", 0L)
|
||||
if (gmsAndroidId == 0L) {
|
||||
gmsAndroidId = GServices.getLong(context.contentResolver, "android_id", 0L)
|
||||
if (gmsAndroidId == 0L) {
|
||||
val androidIdStr =
|
||||
android.provider.Settings.Secure.getString(context.contentResolver, "android_id")
|
||||
gmsAndroidId =
|
||||
androidIdStr?.toLongOrNull(16) ?: (Build.ID.hashCode()
|
||||
.toLong() and 0x7FFFFFFFFFFFFFFFL)
|
||||
}
|
||||
prefs.edit { putLong("gms_android_id", gmsAndroidId) }
|
||||
}
|
||||
|
||||
val userSerial = try {
|
||||
val userManager = context.getSystemService<UserManager>()
|
||||
userManager?.getSerialNumberForUser(Process.myUserHandle()) ?: 0L
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
|
||||
var primaryDeviceId = prefs.getLong("primary_device_id", 0L)
|
||||
val isSystemUser = try {
|
||||
val userManager = context.getSystemService<UserManager>()
|
||||
userManager?.isSystemUser ?: true
|
||||
} catch (_: Exception) {
|
||||
true
|
||||
}
|
||||
if (primaryDeviceId == 0L && isSystemUser) {
|
||||
primaryDeviceId = gmsAndroidId
|
||||
prefs.edit { putLong("primary_device_id", primaryDeviceId) }
|
||||
}
|
||||
|
||||
return DeviceID(
|
||||
iid_token = iidToken,
|
||||
primary_device_id = primaryDeviceId,
|
||||
user_serial = userSerial,
|
||||
gms_android_id = gmsAndroidId
|
||||
)
|
||||
}
|
||||
|
||||
operator fun CountryInfo.Companion.invoke(context: Context): CountryInfo {
|
||||
val simCountries = mutableListOf<String>()
|
||||
val networkCountries = mutableListOf<String>()
|
||||
|
||||
try {
|
||||
val sm = context.getSystemService<SubscriptionManager>()
|
||||
val tm = context.getSystemService<TelephonyManager>()
|
||||
|
||||
sm?.activeSubscriptionInfoList?.forEach { info ->
|
||||
val targetTM =
|
||||
tm?.createForSubscriptionId(info.subscriptionId)
|
||||
|
||||
targetTM?.networkCountryIso?.let { networkCountries.add(it.lowercase()) }
|
||||
info.countryIso?.let { simCountries.add(it.lowercase()) }
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "No permission to access country info", e)
|
||||
}
|
||||
|
||||
if (simCountries.isEmpty()) {
|
||||
val tm = context.getSystemService<TelephonyManager>()
|
||||
val simCountry = tm?.simCountryIso?.lowercase()?.takeIf { it.isNotBlank() }
|
||||
if (simCountry != null) simCountries.add(simCountry)
|
||||
}
|
||||
if (networkCountries.isEmpty()) {
|
||||
val tm = context.getSystemService<TelephonyManager>()
|
||||
val networkCountry = tm?.networkCountryIso?.lowercase()?.takeIf { it.isNotBlank() }
|
||||
if (networkCountry != null) networkCountries.add(networkCountry)
|
||||
}
|
||||
|
||||
return CountryInfo(sim_countries = simCountries, network_countries = networkCountries)
|
||||
}
|
||||
|
||||
suspend operator fun DroidGuardSignals.Companion.invoke(
|
||||
context: Context,
|
||||
iidToken: String,
|
||||
rpc: String
|
||||
): DroidGuardSignals? {
|
||||
val cachedToken = ConstellationStateStore.loadDroidGuardToken(context)
|
||||
if (!cachedToken.isNullOrBlank()) {
|
||||
return DroidGuardSignals(droidguard_token = cachedToken, droidguard_result = "")
|
||||
}
|
||||
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val digest = md.digest(iidToken.toByteArray(Charsets.UTF_8))
|
||||
val iidHash = Base64.encodeToString(
|
||||
digest.copyOf(64),
|
||||
Base64.NO_PADDING or Base64.NO_WRAP
|
||||
).substring(0, 32)
|
||||
|
||||
return try {
|
||||
val client = DroidGuard.getClient(context)
|
||||
val data = mapOf(
|
||||
"iidHash" to iidHash,
|
||||
"rpc" to rpc
|
||||
)
|
||||
val result = client.getResults("constellation_verify", data, null).await()
|
||||
DroidGuardSignals(droidguard_result = result, droidguard_token = "")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "DroidGuard generation failed", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package org.microg.gms.constellation.core.proto.builder
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RequiresApi
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.microg.gms.constellation.core.authManager
|
||||
import org.microg.gms.constellation.core.proto.AuditToken
|
||||
import org.microg.gms.constellation.core.proto.AuditTokenMetadata
|
||||
import org.microg.gms.constellation.core.proto.AuditUuid
|
||||
import org.microg.gms.constellation.core.proto.ClientInfo
|
||||
import org.microg.gms.constellation.core.proto.DeviceID
|
||||
import org.microg.gms.constellation.core.proto.Param
|
||||
import org.microg.gms.constellation.core.proto.RequestHeader
|
||||
import org.microg.gms.constellation.core.proto.RequestTrigger
|
||||
|
||||
fun Param.Companion.getList(extras: Bundle?): List<Param> {
|
||||
if (extras == null) return emptyList()
|
||||
val params = mutableListOf<Param>()
|
||||
val ignoreKeys = setOf("consent_variant_key", "consent_trigger_key", "gaia_access_token")
|
||||
for (key in extras.keySet()) {
|
||||
if (key !in ignoreKeys) {
|
||||
extras.getString(key)?.let { value ->
|
||||
params.add(Param(key = key, value_ = value))
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
fun AuditToken.Companion.generate(): AuditToken {
|
||||
val uuid = java.util.UUID.randomUUID()
|
||||
return AuditToken(
|
||||
metadata = AuditTokenMetadata(
|
||||
uuid = AuditUuid(
|
||||
uuid_msb = uuid.mostSignificantBits,
|
||||
uuid_lsb = uuid.leastSignificantBits
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend operator fun RequestHeader.Companion.invoke(
|
||||
context: Context,
|
||||
sessionId: String,
|
||||
buildContext: RequestBuildContext,
|
||||
rpc: String,
|
||||
triggerType: RequestTrigger.Type = RequestTrigger.Type.TRIGGER_API_CALL,
|
||||
includeClientAuth: Boolean = false,
|
||||
): RequestHeader {
|
||||
val authManager = if (includeClientAuth) context.authManager else null
|
||||
val clientAuth = if (includeClientAuth) {
|
||||
val (signature, timestamp) = authManager!!.signIidToken(buildContext.iidToken)
|
||||
org.microg.gms.constellation.core.proto.ClientAuth(
|
||||
device_id = DeviceID(context, buildContext.iidToken),
|
||||
signature = signature.toByteString(),
|
||||
sign_timestamp = timestamp
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return RequestHeader(
|
||||
client_info = ClientInfo(context, buildContext, rpc),
|
||||
client_auth = clientAuth,
|
||||
session_id = sessionId,
|
||||
trigger = RequestTrigger(type = triggerType)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package org.microg.gms.constellation.core.proto.builder
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.squareup.wire.Instant
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.constellation.core.proto.GaiaAccountSignalType
|
||||
import org.microg.gms.constellation.core.proto.GaiaSignalEntry
|
||||
import org.microg.gms.constellation.core.proto.GaiaSignals
|
||||
import org.microg.gms.constellation.core.proto.GaiaToken
|
||||
|
||||
private const val TAG = "GaiaInfoBuilder"
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val entries = mutableListOf<GaiaSignalEntry>()
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
try {
|
||||
val accounts = accountManager.getAccountsByType("com.google")
|
||||
|
||||
for (account in accounts) {
|
||||
var id = accountManager.getUserData(account, "GoogleUserId")
|
||||
if (id == "") {
|
||||
try {
|
||||
val future = accountManager.getAuthToken(
|
||||
account,
|
||||
"^^_account_id_^^",
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
id = future.result?.getString(AccountManager.KEY_AUTHTOKEN)
|
||||
accountManager.setUserData(account, "GoogleUserId", id)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not retrieve Gaia ID for account ${account.name}", e)
|
||||
continue
|
||||
}
|
||||
}
|
||||
entries.add(
|
||||
GaiaSignalEntry(
|
||||
gaia_id = id,
|
||||
signal_type = GaiaAccountSignalType.GAIA_ACCOUNT_SIGNAL_AUTHENTICATED,
|
||||
timestamp = Instant.now()
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not build Gaia signals", e)
|
||||
}
|
||||
if (entries.isNotEmpty()) GaiaSignals(gaia_signals = entries) else null
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun GaiaToken.Companion.getList(context: Context): List<GaiaToken> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
val accounts = accountManager.getAccountsByType("com.google")
|
||||
accounts.mapNotNull { account ->
|
||||
try {
|
||||
val future = accountManager.getAuthToken(
|
||||
account,
|
||||
"oauth2:https://www.googleapis.com/auth/numberer",
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
val token = future.result?.getString(AccountManager.KEY_AUTHTOKEN)
|
||||
|
||||
if (!token.isNullOrBlank()) GaiaToken(token = token) else null
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not retrieve Gaia token for account ${account.name}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package org.microg.gms.constellation.core.proto.builder
|
||||
|
||||
import android.content.Context
|
||||
import org.microg.gms.constellation.core.AuthManager
|
||||
import org.microg.gms.constellation.core.authManager
|
||||
import org.microg.gms.constellation.core.proto.GaiaToken
|
||||
|
||||
data class RequestBuildContext(
|
||||
val iidToken: String,
|
||||
val gaiaTokens: List<GaiaToken>
|
||||
)
|
||||
|
||||
suspend fun buildRequestContext(
|
||||
context: Context,
|
||||
authManager: AuthManager = context.authManager
|
||||
): RequestBuildContext {
|
||||
return RequestBuildContext(
|
||||
iidToken = authManager.getIidToken(),
|
||||
gaiaTokens = GaiaToken.getList(context)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
@file:SuppressLint("HardwareIds")
|
||||
|
||||
package org.microg.gms.constellation.core.proto.builder
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.telephony.PhoneNumberUtils
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberRequest
|
||||
import com.google.android.gms.constellation.verificationMethods
|
||||
import org.microg.gms.common.Constants
|
||||
import org.microg.gms.constellation.core.ConstellationStateStore
|
||||
import org.microg.gms.constellation.core.proto.ChallengePreference
|
||||
import org.microg.gms.constellation.core.proto.ChallengePreferenceMetadata
|
||||
import org.microg.gms.constellation.core.proto.IdTokenRequest
|
||||
import org.microg.gms.constellation.core.proto.Param
|
||||
import org.microg.gms.constellation.core.proto.RequestHeader
|
||||
import org.microg.gms.constellation.core.proto.RequestTrigger
|
||||
import org.microg.gms.constellation.core.proto.SIMAssociation
|
||||
import org.microg.gms.constellation.core.proto.SIMSlotInfo
|
||||
import org.microg.gms.constellation.core.proto.SyncRequest
|
||||
import org.microg.gms.constellation.core.proto.TelephonyInfo
|
||||
import org.microg.gms.constellation.core.proto.TelephonyPhoneNumberType
|
||||
import org.microg.gms.constellation.core.proto.Verification
|
||||
import org.microg.gms.constellation.core.proto.VerificationAssociation
|
||||
import org.microg.gms.constellation.core.proto.VerificationParam
|
||||
import org.microg.gms.constellation.core.proto.VerificationPolicy
|
||||
|
||||
private const val TAG = "SyncRequestBuilder"
|
||||
|
||||
fun buildImsiToSubscriptionInfoMap(context: Context): Map<String, SubscriptionInfo> {
|
||||
val subscriptionManager =
|
||||
context.getSystemService<SubscriptionManager>() ?: return emptyMap()
|
||||
val telephonyManager =
|
||||
context.getSystemService<TelephonyManager>() ?: return emptyMap()
|
||||
val map = mutableMapOf<String, SubscriptionInfo>()
|
||||
|
||||
try {
|
||||
subscriptionManager.activeSubscriptionInfoList?.forEach { info ->
|
||||
val subsTelephonyManager =
|
||||
telephonyManager.createForSubscriptionId(info.subscriptionId)
|
||||
subsTelephonyManager.subscriberId?.let { imsi ->
|
||||
map[imsi] = info
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "No permission to read SIM info for SubscriptionInfo mapping", e)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun getTelephonyPhoneNumbers(
|
||||
context: Context,
|
||||
subscriptionId: Int
|
||||
): List<SIMAssociation.TelephonyPhoneNumber> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return emptyList()
|
||||
|
||||
val permissions =
|
||||
listOf(Manifest.permission.READ_PHONE_NUMBERS, Manifest.permission.READ_PHONE_STATE)
|
||||
if (permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
it
|
||||
) == PackageManager.PERMISSION_DENIED
|
||||
}) return emptyList()
|
||||
|
||||
val subscriptionManager =
|
||||
context.getSystemService<SubscriptionManager>() ?: return emptyList()
|
||||
|
||||
val sources = intArrayOf(
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER,
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_UICC,
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_IMS
|
||||
)
|
||||
|
||||
return try {
|
||||
sources.map { source ->
|
||||
val number = subscriptionManager.getPhoneNumber(subscriptionId, source)
|
||||
if (number.isNotEmpty()) {
|
||||
SIMAssociation.TelephonyPhoneNumber(
|
||||
phone_number = number,
|
||||
phone_number_type = when (source) {
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER ->
|
||||
TelephonyPhoneNumberType.PHONE_NUMBER_SOURCE_CARRIER
|
||||
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_UICC ->
|
||||
TelephonyPhoneNumberType.PHONE_NUMBER_SOURCE_UICC
|
||||
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_IMS ->
|
||||
TelephonyPhoneNumberType.PHONE_NUMBER_SOURCE_IMS
|
||||
|
||||
else -> TelephonyPhoneNumberType.PHONE_NUMBER_SOURCE_UNSPECIFIED
|
||||
}
|
||||
)
|
||||
} else null
|
||||
}.filterNotNull()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error getting telephony phone numbers", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend operator fun SyncRequest.Companion.invoke(
|
||||
context: Context,
|
||||
sessionId: String,
|
||||
request: VerifyPhoneNumberRequest,
|
||||
includeClientAuth: Boolean = false,
|
||||
callingPackage: String = Constants.GMS_PACKAGE_NAME,
|
||||
triggerType: RequestTrigger.Type = RequestTrigger.Type.TRIGGER_API_CALL
|
||||
): SyncRequest {
|
||||
val buildContext = buildRequestContext(context)
|
||||
return SyncRequest(
|
||||
context = context,
|
||||
sessionId = sessionId,
|
||||
request = request,
|
||||
buildContext = buildContext,
|
||||
includeClientAuth = includeClientAuth,
|
||||
callingPackage = callingPackage,
|
||||
triggerType = triggerType
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend operator fun SyncRequest.Companion.invoke(
|
||||
context: Context,
|
||||
sessionId: String,
|
||||
request: VerifyPhoneNumberRequest,
|
||||
buildContext: RequestBuildContext,
|
||||
imsiToInfoMap: Map<String, SubscriptionInfo> = buildImsiToSubscriptionInfoMap(context),
|
||||
includeClientAuth: Boolean = false,
|
||||
callingPackage: String = Constants.GMS_PACKAGE_NAME,
|
||||
triggerType: RequestTrigger.Type = RequestTrigger.Type.TRIGGER_API_CALL
|
||||
): SyncRequest {
|
||||
val apiParamsList = Param.getList(request.extras)
|
||||
|
||||
val verificationParams = request.targetedSims.map {
|
||||
VerificationParam(key = it.imsi, value_ = it.phoneNumberHint)
|
||||
}
|
||||
|
||||
val structuredParams = VerificationPolicy(
|
||||
policy_id = request.policyId,
|
||||
max_verification_age_hours = request.timeout,
|
||||
id_token_request = IdTokenRequest(
|
||||
certificate_hash = request.idTokenRequest.idToken,
|
||||
token_nonce = request.idTokenRequest.subscriberHash
|
||||
),
|
||||
calling_package = callingPackage,
|
||||
params = verificationParams
|
||||
)
|
||||
|
||||
val verifications = imsiToInfoMap.map { (imsi, subscriptionInfo) ->
|
||||
val subscriptionId = subscriptionInfo.subscriptionId
|
||||
val slotIndex = subscriptionInfo.simSlotIndex
|
||||
val phoneNumber = PhoneNumberUtils.formatNumberToE164(subscriptionInfo.number, subscriptionInfo.countryIso) ?: ""
|
||||
val iccid = subscriptionInfo.iccId ?: ""
|
||||
|
||||
Verification(
|
||||
status = Verification.Status.STATUS_NONE,
|
||||
association = VerificationAssociation(
|
||||
sim = SIMAssociation(
|
||||
sim_info = SIMAssociation.SIMInfo(
|
||||
imsi = listOf(imsi),
|
||||
sim_readable_number = phoneNumber,
|
||||
telephony_phone_number = getTelephonyPhoneNumbers(context, subscriptionId),
|
||||
iccid = iccid
|
||||
),
|
||||
gaia_tokens = buildContext.gaiaTokens,
|
||||
sim_slot = SIMSlotInfo(
|
||||
slot_index = slotIndex,
|
||||
subscription_id = subscriptionId
|
||||
)
|
||||
)
|
||||
),
|
||||
telephony_info = TelephonyInfo(context, subscriptionId),
|
||||
structured_api_params = structuredParams,
|
||||
api_params = apiParamsList,
|
||||
challenge_preference = ChallengePreference(
|
||||
capabilities = request.verificationMethods,
|
||||
metadata = ChallengePreferenceMetadata()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return SyncRequest(
|
||||
verifications = verifications,
|
||||
header_ = RequestHeader(
|
||||
context,
|
||||
sessionId,
|
||||
buildContext,
|
||||
"sync",
|
||||
triggerType,
|
||||
includeClientAuth,
|
||||
),
|
||||
verification_tokens = ConstellationStateStore.loadVerificationTokens(context)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
@file:SuppressLint("HardwareIds")
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.microg.gms.constellation.core.proto.builder
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.UserManager
|
||||
import android.telephony.ServiceState
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import org.microg.gms.constellation.core.proto.NetworkSignal
|
||||
import org.microg.gms.constellation.core.proto.SimNetworkInfo
|
||||
import org.microg.gms.constellation.core.proto.SimOperatorInfo
|
||||
import org.microg.gms.constellation.core.proto.TelephonyInfo
|
||||
import java.security.MessageDigest
|
||||
|
||||
private const val TAG = "TelephonyInfoBuilder"
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
operator fun TelephonyInfo.Companion.invoke(context: Context, subscriptionId: Int): TelephonyInfo {
|
||||
val tm = context.getSystemService<TelephonyManager>()
|
||||
val subscriptionManager = context.getSystemService<SubscriptionManager>()
|
||||
val targetTm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && subscriptionId >= 0) {
|
||||
tm?.createForSubscriptionId(subscriptionId)
|
||||
} else tm
|
||||
|
||||
val simState = when (targetTm?.simState) {
|
||||
TelephonyManager.SIM_STATE_READY -> TelephonyInfo.SimState.SIM_STATE_READY
|
||||
else -> TelephonyInfo.SimState.SIM_STATE_NOT_READY
|
||||
}
|
||||
|
||||
val phoneType = when (targetTm?.phoneType) {
|
||||
TelephonyManager.PHONE_TYPE_GSM -> TelephonyInfo.PhoneType.PHONE_TYPE_GSM
|
||||
TelephonyManager.PHONE_TYPE_CDMA -> TelephonyInfo.PhoneType.PHONE_TYPE_CDMA
|
||||
TelephonyManager.PHONE_TYPE_SIP -> TelephonyInfo.PhoneType.PHONE_TYPE_SIP
|
||||
else -> TelephonyInfo.PhoneType.PHONE_TYPE_UNKNOWN
|
||||
}
|
||||
|
||||
val networkRoaming = if (targetTm?.isNetworkRoaming == true) {
|
||||
TelephonyInfo.RoamingState.ROAMING_ROAMING
|
||||
} else {
|
||||
TelephonyInfo.RoamingState.ROAMING_HOME
|
||||
}
|
||||
|
||||
val cm = context.getSystemService<ConnectivityManager>()
|
||||
val activeNetwork = cm?.activeNetworkInfo
|
||||
val connectivityState = when {
|
||||
activeNetwork == null -> TelephonyInfo.ConnectivityState.CONNECTIVITY_UNKNOWN
|
||||
activeNetwork.isRoaming -> TelephonyInfo.ConnectivityState.CONNECTIVITY_ROAMING
|
||||
else -> TelephonyInfo.ConnectivityState.CONNECTIVITY_HOME
|
||||
}
|
||||
|
||||
val simInfo = SimNetworkInfo(
|
||||
country_iso = targetTm?.simCountryIso?.lowercase() ?: "",
|
||||
operator_ = targetTm?.simOperator ?: "",
|
||||
operator_name = targetTm?.simOperatorName ?: ""
|
||||
)
|
||||
|
||||
val networkInfo = SimNetworkInfo(
|
||||
country_iso = targetTm?.networkCountryIso?.lowercase() ?: "",
|
||||
operator_ = targetTm?.networkOperator ?: "",
|
||||
operator_name = targetTm?.networkOperatorName ?: ""
|
||||
)
|
||||
|
||||
val (activeSubCount, activeSubCountMax) = getActiveSubCounts(subscriptionManager)
|
||||
|
||||
return TelephonyInfo(
|
||||
phone_type = phoneType,
|
||||
group_id_level1 = targetTm?.groupIdLevel1 ?: "",
|
||||
sim_info = simInfo,
|
||||
network_info = networkInfo,
|
||||
network_roaming = networkRoaming,
|
||||
connectivity_state = connectivityState,
|
||||
sms_capability = getSmsCapability(context, targetTm),
|
||||
active_sub_count = activeSubCount,
|
||||
active_sub_count_max = activeSubCountMax,
|
||||
sim_state = simState,
|
||||
service_state = getServiceState(targetTm),
|
||||
sim_carrier_id = getSimCarrierId(targetTm),
|
||||
is_embedded = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSmsCapability(
|
||||
context: Context,
|
||||
telephonyManager: TelephonyManager?
|
||||
): TelephonyInfo.SmsCapability {
|
||||
val hasReadSms =
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
val hasSendSms =
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
|
||||
if (!hasReadSms || !hasSendSms) {
|
||||
return TelephonyInfo.SmsCapability.SMS_DEFAULT_CAPABILITY
|
||||
}
|
||||
|
||||
val isSmsRestricted =
|
||||
context.getSystemService<UserManager>()?.userRestrictions?.getBoolean(UserManager.DISALLOW_SMS) ==
|
||||
true
|
||||
if (isSmsRestricted) {
|
||||
return TelephonyInfo.SmsCapability.SMS_RESTRICTED
|
||||
}
|
||||
|
||||
return if (telephonyManager?.isSmsCapable == true) {
|
||||
TelephonyInfo.SmsCapability.SMS_CAPABLE
|
||||
} else {
|
||||
TelephonyInfo.SmsCapability.SMS_NOT_CAPABLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActiveSubCounts(subscriptionManager: SubscriptionManager?): Pair<Int, Int> {
|
||||
if (subscriptionManager == null) {
|
||||
return 0 to 0
|
||||
}
|
||||
|
||||
return try {
|
||||
subscriptionManager.activeSubscriptionInfoCount to
|
||||
subscriptionManager.activeSubscriptionInfoCountMax
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Could not retrieve active subscription counts", e)
|
||||
0 to 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getServiceState(telephonyManager: TelephonyManager?): TelephonyInfo.ServiceState {
|
||||
val state = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
telephonyManager?.serviceState?.state
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Could not retrieve service state", e)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return when (state) {
|
||||
ServiceState.STATE_IN_SERVICE -> TelephonyInfo.ServiceState.SERVICE_STATE_IN_SERVICE
|
||||
ServiceState.STATE_OUT_OF_SERVICE -> TelephonyInfo.ServiceState.SERVICE_STATE_OUT_OF_SERVICE
|
||||
ServiceState.STATE_EMERGENCY_ONLY -> TelephonyInfo.ServiceState.SERVICE_STATE_EMERGENCY_ONLY
|
||||
ServiceState.STATE_POWER_OFF -> TelephonyInfo.ServiceState.SERVICE_STATE_POWER_OFF
|
||||
else -> TelephonyInfo.ServiceState.SERVICE_STATE_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSimCarrierId(telephonyManager: TelephonyManager?): Long {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
telephonyManager?.simCarrierId?.toLong() ?: -1L
|
||||
} else {
|
||||
-1L
|
||||
}
|
||||
}
|
||||
|
||||
fun NetworkSignal.Companion.getList(context: Context): List<NetworkSignal> {
|
||||
val connectivityInfos = mutableListOf<NetworkSignal>()
|
||||
try {
|
||||
val cm = context.getSystemService<ConnectivityManager>()
|
||||
cm?.activeNetworkInfo?.let { networkInfo ->
|
||||
val type = when (networkInfo.type) {
|
||||
ConnectivityManager.TYPE_WIFI -> NetworkSignal.Type.TYPE_WIFI
|
||||
ConnectivityManager.TYPE_MOBILE -> NetworkSignal.Type.TYPE_MOBILE
|
||||
else -> NetworkSignal.Type.TYPE_UNKNOWN
|
||||
}
|
||||
val state = when {
|
||||
networkInfo.isConnected -> NetworkSignal.State.STATE_CONNECTED
|
||||
networkInfo.isConnectedOrConnecting && !networkInfo.isConnected -> NetworkSignal.State.STATE_CONNECTING
|
||||
else -> NetworkSignal.State.STATE_DISCONNECTED
|
||||
}
|
||||
val availability =
|
||||
if (networkInfo.isAvailable) NetworkSignal.Availability.AVAILABLE else NetworkSignal.Availability.NOT_AVAILABLE
|
||||
|
||||
connectivityInfos.add(
|
||||
NetworkSignal(
|
||||
type = type,
|
||||
availability = availability,
|
||||
state = state
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not retrieve network info", e)
|
||||
}
|
||||
return connectivityInfos
|
||||
}
|
||||
|
||||
fun SimOperatorInfo.Companion.getList(context: Context): List<SimOperatorInfo> {
|
||||
val infos = mutableListOf<SimOperatorInfo>()
|
||||
try {
|
||||
val sm = context.getSystemService<SubscriptionManager>()
|
||||
val tm = context.getSystemService<TelephonyManager>()
|
||||
|
||||
sm?.activeSubscriptionInfoList?.forEach { info ->
|
||||
val targetTM = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
tm?.createForSubscriptionId(info.subscriptionId)
|
||||
} else tm
|
||||
|
||||
targetTM?.subscriberId?.let { imsi ->
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val hash = Base64.encodeToString(
|
||||
md.digest(imsi.toByteArray(Charsets.UTF_8)),
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
|
||||
val simOperator = targetTM.simOperator ?: ""
|
||||
infos.add(
|
||||
SimOperatorInfo(
|
||||
sim_operator = simOperator,
|
||||
imsi_hash = hash
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "No permission to access SIM info for operator list", e)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error hashing IMSI", e)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.N)
|
||||
|
||||
package org.microg.gms.constellation.core.verification
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import org.microg.gms.constellation.core.proto.CarrierIdChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.CarrierIdError
|
||||
import org.microg.gms.constellation.core.proto.Challenge
|
||||
import org.microg.gms.constellation.core.proto.ChallengeResponse
|
||||
|
||||
private const val TAG = "CarrierIdVerifier"
|
||||
|
||||
internal data class CarrierIdSession(
|
||||
val challengeId: String,
|
||||
val subId: Int,
|
||||
var attempts: Int = 0,
|
||||
) {
|
||||
fun matches(challengeId: String, subId: Int): Boolean {
|
||||
return this.challengeId == challengeId && this.subId == subId
|
||||
}
|
||||
}
|
||||
|
||||
fun Challenge.verifyCarrierId(context: Context, subId: Int): ChallengeResponse {
|
||||
val carrierChallenge = carrier_id_challenge ?: return failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_UNKNOWN_ERROR,
|
||||
"Carrier challenge data missing"
|
||||
)
|
||||
val challengeData = carrierChallenge.isim_request.takeIf { it.isNotEmpty() }
|
||||
?: return failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_UNKNOWN_ERROR,
|
||||
"Carrier challenge data missing"
|
||||
)
|
||||
if (subId == -1) return failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_NO_SIM,
|
||||
"No active subscription for carrier auth"
|
||||
)
|
||||
|
||||
val telephonyManager = context.getSystemService<TelephonyManager>()
|
||||
?: return failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_NOT_SUPPORTED,
|
||||
"TelephonyManager unavailable"
|
||||
)
|
||||
val targetManager =
|
||||
telephonyManager.createForSubscriptionId(subId)
|
||||
if (challengeData.startsWith("[ts43]")) {
|
||||
// Not supported for now, try to get the server to dispatch something different
|
||||
return failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_NOT_SUPPORTED,
|
||||
"TS43-prefixed Carrier ID challenge not supported"
|
||||
)
|
||||
}
|
||||
|
||||
val appType = carrierChallenge.app_type.takeIf { it != 0 } ?: TelephonyManager.APPTYPE_USIM
|
||||
|
||||
return try {
|
||||
val response =
|
||||
targetManager.getIccAuthentication(appType, carrierChallenge.auth_type, challengeData)
|
||||
if (response.isNullOrEmpty()) {
|
||||
failure(CarrierIdError.CARRIER_ID_ERROR_NULL_RESPONSE, "Null ISIM response")
|
||||
} else {
|
||||
ChallengeResponse(
|
||||
carrier_id_response = CarrierIdChallengeResponse(
|
||||
isim_response = response,
|
||||
carrier_id_error = CarrierIdError.CARRIER_ID_ERROR_NO_ERROR
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Unable to read subscription for carrier auth", e)
|
||||
failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_UNABLE_TO_READ_SUBSCRIPTION,
|
||||
e.message ?: "SecurityException"
|
||||
)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
Log.w(TAG, "Carrier auth API unavailable", e)
|
||||
failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_NOT_SUPPORTED,
|
||||
e.message ?: "UnsupportedOperationException"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Carrier auth failed", e)
|
||||
failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_REFLECTION_ERROR,
|
||||
e.message ?: "Reflection or platform error"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun retryExceededCarrierId(): ChallengeResponse {
|
||||
return failure(
|
||||
CarrierIdError.CARRIER_ID_ERROR_RETRY_ATTEMPT_EXCEEDED,
|
||||
"Carrier ID retry attempt exceeded"
|
||||
)
|
||||
}
|
||||
|
||||
private fun failure(status: CarrierIdError, message: String): ChallengeResponse {
|
||||
Log.w(TAG, message)
|
||||
return ChallengeResponse(
|
||||
carrier_id_response = CarrierIdChallengeResponse(
|
||||
isim_response = "",
|
||||
carrier_id_error = status
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
|
||||
package org.microg.gms.constellation.core.verification
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.squareup.wire.GrpcException
|
||||
import com.squareup.wire.GrpcStatus
|
||||
import kotlinx.coroutines.delay
|
||||
import org.microg.gms.constellation.core.ConstellationStateStore
|
||||
import org.microg.gms.constellation.core.RpcClient
|
||||
import org.microg.gms.constellation.core.proto.ChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.ProceedRequest
|
||||
import org.microg.gms.constellation.core.proto.RequestHeader
|
||||
import org.microg.gms.constellation.core.proto.Verification
|
||||
import org.microg.gms.constellation.core.proto.VerificationMethod
|
||||
import org.microg.gms.constellation.core.proto.builder.RequestBuildContext
|
||||
import org.microg.gms.constellation.core.proto.builder.invoke
|
||||
import org.microg.gms.constellation.core.state
|
||||
|
||||
object ChallengeProcessor {
|
||||
private const val TAG = "ChallengeProcessor"
|
||||
private const val MAX_PROCEED_ROUNDS = 16
|
||||
|
||||
private fun challengeTimeRemainingMillis(verification: Verification): Long? {
|
||||
val expiry = verification.pending_verification_info?.challenge?.expiry_time ?: return null
|
||||
val targetMillis = expiry.timestamp?.toEpochMilli() ?: return null
|
||||
val referenceMillis = expiry.now?.toEpochMilli() ?: return null
|
||||
return targetMillis - referenceMillis
|
||||
}
|
||||
|
||||
suspend fun process(
|
||||
context: Context,
|
||||
sessionId: String,
|
||||
imsiToInfoMap: Map<String, SubscriptionInfo>,
|
||||
buildContext: RequestBuildContext,
|
||||
verification: Verification,
|
||||
): Verification {
|
||||
var currentVerification = verification
|
||||
var moSession: MoSmsSession? = null
|
||||
var carrierIdSession: CarrierIdSession? = null
|
||||
|
||||
for (attempt in 1..MAX_PROCEED_ROUNDS) {
|
||||
if (currentVerification.state != Verification.State.PENDING) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Verification state: ${currentVerification.state}. Stopping sequential verification."
|
||||
)
|
||||
return currentVerification
|
||||
}
|
||||
|
||||
val challenge = currentVerification.pending_verification_info?.challenge
|
||||
if (challenge == null) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Attempt $attempt: Pending verification but no challenge found. Stopping."
|
||||
)
|
||||
return currentVerification
|
||||
}
|
||||
|
||||
val challengeId = challenge.challenge_id?.id ?: ""
|
||||
val remainingMillis = challengeTimeRemainingMillis(currentVerification)
|
||||
if (remainingMillis != null && remainingMillis <= 0L) {
|
||||
if (challenge.type == VerificationMethod.MT_SMS) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Attempt $attempt: Challenge $challengeId expired before MT wait. Proceeding with empty MT response."
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "Attempt $attempt: Challenge $challengeId expired before proceed")
|
||||
return currentVerification
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"Attempt $attempt: Solving challenge ID: $challengeId, Type: ${challenge.type}"
|
||||
)
|
||||
|
||||
val challengeImsi = currentVerification.association?.sim?.sim_info?.imsi?.firstOrNull()
|
||||
val info = imsiToInfoMap[challengeImsi]
|
||||
val subId = info?.subscriptionId ?: -1
|
||||
|
||||
if (challenge.type != VerificationMethod.MO_SMS) {
|
||||
moSession = null
|
||||
}
|
||||
if (challenge.type == VerificationMethod.CARRIER_ID && challenge.ts43_challenge == null) {
|
||||
carrierIdSession = carrierIdSession?.takeIf { it.matches(challengeId, subId) }
|
||||
?: CarrierIdSession(challengeId, subId)
|
||||
} else {
|
||||
carrierIdSession = null
|
||||
}
|
||||
|
||||
val challengeResponse: ChallengeResponse? = when (challenge.type) {
|
||||
VerificationMethod.TS43 -> challenge.ts43_challenge?.verify(context, subId)
|
||||
|
||||
VerificationMethod.CARRIER_ID -> if (challenge.ts43_challenge != null)
|
||||
challenge.ts43_challenge.verify(context, subId)
|
||||
else {
|
||||
carrierIdSession!!.attempts += 1
|
||||
|
||||
if (carrierIdSession!!.attempts >= 4) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Attempt $attempt: Carrier ID challenge $challengeId is still pending after retry-exceeded response. Stopping."
|
||||
)
|
||||
return currentVerification
|
||||
}
|
||||
|
||||
if (carrierIdSession.attempts >= 3) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Attempt $attempt: Carrier ID challenge $challengeId exceeded retry budget. Proceeding with retry-exceeded response."
|
||||
)
|
||||
retryExceededCarrierId()
|
||||
} else {
|
||||
challenge.verifyCarrierId(context, subId)
|
||||
}
|
||||
}
|
||||
|
||||
VerificationMethod.MT_SMS -> challenge.mt_challenge?.verify(subId, remainingMillis)
|
||||
|
||||
VerificationMethod.MO_SMS -> challenge.mo_challenge?.let { moChallenge ->
|
||||
if (moSession?.matches(challengeId, subId) != true) {
|
||||
moChallenge.startSession(context, challengeId, subId).also {
|
||||
moSession = it
|
||||
}.response
|
||||
} else {
|
||||
val delayMillis = moSession!!.nextPollingDelayMillis(remainingMillis)
|
||||
if (delayMillis > 0L) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Attempt $attempt: Challenge $challengeId still pending. Waiting ${delayMillis}ms before next MO proceed ping."
|
||||
)
|
||||
delay(delayMillis)
|
||||
}
|
||||
moSession!!.response
|
||||
}
|
||||
}
|
||||
|
||||
VerificationMethod.REGISTERED_SMS -> challenge.registered_sms_challenge?.verify(
|
||||
context,
|
||||
subId
|
||||
)
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unsupported verification method: ${challenge.type}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (challengeResponse == null) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Attempt $attempt: Challenge verification failed or returned no response."
|
||||
)
|
||||
// GMS continues looping if IMSI doesn't match or other issues, but here we return to avoid infinite retries if verifier is broken
|
||||
return currentVerification
|
||||
}
|
||||
|
||||
Log.d(TAG, "Attempt $attempt: Challenge successfully solved. Proceeding...")
|
||||
val proceedHeader = RequestHeader(
|
||||
context,
|
||||
sessionId,
|
||||
buildContext,
|
||||
"proceed",
|
||||
includeClientAuth = true,
|
||||
)
|
||||
val proceedRequest = ProceedRequest(
|
||||
verification = currentVerification,
|
||||
challenge_response = challengeResponse,
|
||||
header_ = proceedHeader
|
||||
)
|
||||
val proceedResponse = try {
|
||||
RpcClient.phoneDeviceVerificationClient.Proceed().execute(proceedRequest)
|
||||
} catch (e: GrpcException) {
|
||||
if (e.grpcStatus == GrpcStatus.PERMISSION_DENIED ||
|
||||
e.grpcStatus == GrpcStatus.UNAUTHENTICATED
|
||||
) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache..."
|
||||
)
|
||||
ConstellationStateStore.clearDroidGuardToken(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
ConstellationStateStore.storeProceedResponse(context, proceedResponse)
|
||||
currentVerification = proceedResponse.verification ?: currentVerification
|
||||
|
||||
if (challenge.type == VerificationMethod.CARRIER_ID && challenge.ts43_challenge == null) {
|
||||
val nextChallenge = currentVerification.pending_verification_info?.challenge
|
||||
val sameCarrierIdChallenge =
|
||||
nextChallenge?.type == VerificationMethod.CARRIER_ID &&
|
||||
nextChallenge.challenge_id?.id == challengeId &&
|
||||
nextChallenge.ts43_challenge == null
|
||||
|
||||
if (!sameCarrierIdChallenge) {
|
||||
carrierIdSession = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "Exhausted all $MAX_PROCEED_ROUNDS proceed rounds, record is still pending.")
|
||||
return currentVerification
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
|
||||
package org.microg.gms.constellation.core.verification
|
||||
|
||||
import android.Manifest
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.telephony.SmsManager
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.microg.gms.constellation.core.proto.ChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.MOChallengeResponseData
|
||||
import org.microg.gms.constellation.core.proto.MoChallenge
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val TAG = "MoSmsVerifier"
|
||||
private const val ACTION_MO_SMS_SENT = "org.microg.gms.constellation.core.MO_SMS_SENT"
|
||||
private const val DEFAULT_MO_POLLING_INTERVALS_MILLIS =
|
||||
"4000,1000,1000,3000,5000,5000,5000,5000,30000,30000,30000,240000,600000,300000"
|
||||
|
||||
data class MoSmsSession(
|
||||
val challengeId: String,
|
||||
val subId: Int,
|
||||
val response: ChallengeResponse,
|
||||
private val pollingIntervalsMillis: List<Long>,
|
||||
private var nextPollingIndex: Int = 0,
|
||||
) {
|
||||
fun matches(challengeId: String, subId: Int): Boolean {
|
||||
return this.challengeId == challengeId && this.subId == subId
|
||||
}
|
||||
|
||||
fun nextPollingDelayMillis(remainingMillis: Long?): Long {
|
||||
val configuredDelay = pollingIntervalsMillis.getOrNull(nextPollingIndex)
|
||||
if (configuredDelay != null) {
|
||||
nextPollingIndex += 1
|
||||
}
|
||||
val delayMillis = configuredDelay ?: remainingMillis ?: 0L
|
||||
return if (remainingMillis != null) {
|
||||
delayMillis.coerceAtMost(remainingMillis.coerceAtLeast(0L))
|
||||
} else {
|
||||
delayMillis.coerceAtLeast(0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun MoChallenge.startSession(
|
||||
context: Context,
|
||||
challengeId: String,
|
||||
subId: Int
|
||||
): MoSmsSession {
|
||||
return MoSmsSession(
|
||||
challengeId = challengeId,
|
||||
subId = subId,
|
||||
response = sendOnce(context, subId),
|
||||
pollingIntervalsMillis = pollingIntervalsMillis()
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MoChallenge.sendOnce(context: Context, subId: Int): ChallengeResponse {
|
||||
if (proxy_number.isEmpty() || sms.isEmpty()) {
|
||||
return failedMoResponse()
|
||||
}
|
||||
if (context.checkCallingOrSelfPermission(Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.w(TAG, "SEND_SMS permission missing")
|
||||
return failedMoResponse()
|
||||
}
|
||||
|
||||
if (subId != -1 && !isActiveSubscription(context, subId)) {
|
||||
return ChallengeResponse(
|
||||
mo_response = MOChallengeResponseData(
|
||||
status = MOChallengeResponseData.Status.NO_ACTIVE_SUBSCRIPTION
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val smsManager = resolveSmsManager(context, subId) ?: return ChallengeResponse(
|
||||
mo_response = MOChallengeResponseData(
|
||||
status = MOChallengeResponseData.Status.NO_SMS_MANAGER
|
||||
)
|
||||
)
|
||||
|
||||
val port = data_sms_info?.destination_port ?: 0
|
||||
val action = ACTION_MO_SMS_SENT
|
||||
val messageId = UUID.randomUUID().toString()
|
||||
val sentIntent = Intent(action).apply {
|
||||
`package` = context.packageName
|
||||
putExtra("message_id", messageId)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
messageId.hashCode(),
|
||||
sentIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||
)
|
||||
|
||||
Log.d(TAG, "Sending MO SMS to $proxy_number with messageId: $messageId")
|
||||
|
||||
val response = withTimeoutOrNull(30_000L) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val receiver = MoSmsSentReceiver(action, messageId, continuation)
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
receiver,
|
||||
IntentFilter(action),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
runCatching { context.unregisterReceiver(receiver) }
|
||||
}
|
||||
|
||||
try {
|
||||
if (port > 0) {
|
||||
smsManager.sendDataMessage(
|
||||
proxy_number,
|
||||
null,
|
||||
port.toShort(),
|
||||
sms.encodeToByteArray(),
|
||||
pendingIntent,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
smsManager.sendTextMessage(
|
||||
proxy_number,
|
||||
null,
|
||||
sms,
|
||||
pendingIntent,
|
||||
null
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initiate MO SMS send", e)
|
||||
runCatching { context.unregisterReceiver(receiver) }
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(failedMoResponse())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return response ?: failedMoResponse()
|
||||
}
|
||||
|
||||
private class MoSmsSentReceiver(
|
||||
private val action: String,
|
||||
private val messageId: String,
|
||||
private val continuation: CancellableContinuation<ChallengeResponse>
|
||||
) : BroadcastReceiver() {
|
||||
override fun onReceive(receiverContext: Context, intent: Intent) {
|
||||
if (intent.action != action) return
|
||||
if (intent.getStringExtra("message_id") != messageId) return
|
||||
|
||||
val smsResultCode = resultCode
|
||||
val smsErrorCode = intent.getIntExtra("errorCode", -1)
|
||||
|
||||
Log.d(TAG, "MO SMS sent result: $smsResultCode, error: $smsErrorCode")
|
||||
runCatching { receiverContext.unregisterReceiver(this) }
|
||||
if (!continuation.isActive) return
|
||||
|
||||
continuation.resume(
|
||||
ChallengeResponse(
|
||||
mo_response = MOChallengeResponseData(
|
||||
status = if (smsResultCode == -1) {
|
||||
MOChallengeResponseData.Status.COMPLETED
|
||||
} else {
|
||||
MOChallengeResponseData.Status.FAILED_TO_SEND_MO
|
||||
},
|
||||
sms_result_code = smsResultCode.toLong(),
|
||||
sms_error_code = smsErrorCode.toLong()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MoChallenge.pollingIntervalsMillis(): List<Long> {
|
||||
val configuredIntervals = polling_intervals.parsePollingIntervals()
|
||||
return configuredIntervals.ifEmpty {
|
||||
DEFAULT_MO_POLLING_INTERVALS_MILLIS.parsePollingIntervals()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.parsePollingIntervals(): List<Long> {
|
||||
return split(',')
|
||||
.mapNotNull { it.trim().toLongOrNull() }
|
||||
.filter { it > 0L }
|
||||
}
|
||||
|
||||
private fun failedMoResponse(): ChallengeResponse {
|
||||
return ChallengeResponse(
|
||||
mo_response = MOChallengeResponseData(
|
||||
status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun isActiveSubscription(context: Context, subId: Int): Boolean {
|
||||
if (subId == -1) return true
|
||||
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_PHONE_STATE
|
||||
) == PackageManager.PERMISSION_DENIED
|
||||
) {
|
||||
Log.e(TAG, "Permission not granted")
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
val subManager = context.getSystemService<SubscriptionManager>() ?: return false
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
subManager.isActiveSubscriptionId(subId)
|
||||
} else {
|
||||
subManager.getActiveSubscriptionInfo(subId) != null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to query active subscription for $subId", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun resolveSmsManager(context: Context, subId: Int): SmsManager? {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val manager = context.getSystemService<SmsManager>()
|
||||
if (subId != -1) {
|
||||
manager?.createForSubscriptionId(subId)
|
||||
} else {
|
||||
manager
|
||||
}
|
||||
} else {
|
||||
if (subId != -1) {
|
||||
SmsManager.getSmsManagerForSubscriptionId(subId)
|
||||
} else {
|
||||
SmsManager.getDefault()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to resolve SmsManager for subId: $subId", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
package org.microg.gms.constellation.core.verification
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.provider.Telephony
|
||||
import android.telephony.SmsMessage
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.microg.gms.constellation.core.proto.ChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.MTChallenge
|
||||
import org.microg.gms.constellation.core.proto.MTChallengeResponseData
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val TAG = "MtSmsVerifier"
|
||||
|
||||
suspend fun MTChallenge.verify(
|
||||
subId: Int,
|
||||
timeoutMillis: Long? = null
|
||||
): ChallengeResponse? {
|
||||
val expectedBody = sms.takeIf { it.isNotEmpty() } ?: return null
|
||||
val inbox = MtSmsInboxRegistry.get(subId)
|
||||
val effectiveTimeoutMillis = timeoutMillis?.coerceAtLeast(0L) ?: TimeUnit.MINUTES.toMillis(30)
|
||||
|
||||
Log.d(TAG, "Waiting for MT SMS containing challenge string")
|
||||
|
||||
val match = withTimeoutOrNull(effectiveTimeoutMillis) {
|
||||
inbox.awaitMatch(expectedBody)
|
||||
}
|
||||
|
||||
if (match == null) {
|
||||
Log.w(TAG, "Timed out waiting for MT SMS, proceeding with empty response")
|
||||
}
|
||||
|
||||
val response = match ?: ReceivedSms(body = "", sender = "")
|
||||
return ChallengeResponse(
|
||||
mt_response = MTChallengeResponseData(
|
||||
sms = response.body,
|
||||
sender = response.sender
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
internal object MtSmsInboxRegistry {
|
||||
private val inboxes = ConcurrentHashMap<Int, MtSmsInbox>()
|
||||
|
||||
fun prepare(context: Context, subIds: Iterable<Int>) {
|
||||
dispose()
|
||||
|
||||
val effectiveSubIds = subIds.distinct().ifEmpty { listOf(-1) }
|
||||
for (subId in effectiveSubIds) {
|
||||
inboxes[subId] = MtSmsInbox(context.applicationContext, subId)
|
||||
}
|
||||
}
|
||||
|
||||
fun get(subId: Int): MtSmsInbox {
|
||||
return inboxes[subId]
|
||||
?: error("MT SMS inbox for subId=$subId was not initialized")
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
val currentInboxes = inboxes.values.toList()
|
||||
inboxes.clear()
|
||||
for (inbox in currentInboxes) {
|
||||
inbox.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ReceivedSms(
|
||||
val body: String,
|
||||
val sender: String
|
||||
)
|
||||
|
||||
private data class PendingMatch(
|
||||
val expectedBody: String,
|
||||
val continuation: CancellableContinuation<ReceivedSms?>
|
||||
)
|
||||
|
||||
internal class MtSmsInbox(
|
||||
context: Context,
|
||||
private val subId: Int
|
||||
) {
|
||||
private val context = context.applicationContext
|
||||
private val lock = Any()
|
||||
private val bufferedMessages = mutableListOf<ReceivedSms>()
|
||||
private val pendingMatches = mutableListOf<PendingMatch>()
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return
|
||||
|
||||
val receivedSubId = intent.getIntExtra(
|
||||
"android.telephony.extra.SUBSCRIPTION_INDEX",
|
||||
intent.getIntExtra("subscription", -1)
|
||||
)
|
||||
if (subId != -1 && receivedSubId != subId) return
|
||||
|
||||
onMessagesReceived(Telephony.Sms.Intents.getMessagesFromIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION).apply {
|
||||
priority = 1000
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
receiver,
|
||||
filter,
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitMatch(expectedBody: String): ReceivedSms? {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
synchronized(lock) {
|
||||
bufferedMessages.firstOrNull { it.body.contains(expectedBody) }?.let {
|
||||
continuation.resume(it)
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
val pendingMatch = PendingMatch(expectedBody, continuation)
|
||||
pendingMatches += pendingMatch
|
||||
continuation.invokeOnCancellation {
|
||||
synchronized(lock) {
|
||||
pendingMatches.remove(pendingMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMessagesReceived(messages: Array<SmsMessage>) {
|
||||
val receivedMessages = messages.mapNotNull { message ->
|
||||
val body = message.messageBody ?: return@mapNotNull null
|
||||
ReceivedSms(
|
||||
body = body,
|
||||
sender = message.originatingAddress ?: ""
|
||||
)
|
||||
}
|
||||
if (receivedMessages.isEmpty()) return
|
||||
|
||||
synchronized(lock) {
|
||||
bufferedMessages += receivedMessages
|
||||
for (receivedMessage in receivedMessages) {
|
||||
val iterator = pendingMatches.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val pendingMatch = iterator.next()
|
||||
if (!receivedMessage.body.contains(pendingMatch.expectedBody)) continue
|
||||
|
||||
iterator.remove()
|
||||
Log.d(TAG, "Matching MT SMS received from ${receivedMessage.sender}")
|
||||
if (pendingMatch.continuation.isActive) {
|
||||
pendingMatch.continuation.resume(receivedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
synchronized(lock) {
|
||||
pendingMatches.clear()
|
||||
bufferedMessages.clear()
|
||||
}
|
||||
try {
|
||||
context.unregisterReceiver(receiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
|
||||
package org.microg.gms.constellation.core.verification
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.provider.Telephony
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.microg.gms.constellation.core.VerificationSettingsPhenotypes
|
||||
import org.microg.gms.constellation.core.proto.ChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.RegisteredSmsChallenge
|
||||
import org.microg.gms.constellation.core.proto.RegisteredSmsChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.RegisteredSmsChallengeResponseItem
|
||||
import org.microg.gms.constellation.core.proto.RegisteredSmsChallengeResponsePayload
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val TAG = "RegisteredSmsVerifier"
|
||||
|
||||
fun RegisteredSmsChallenge.verify(context: Context, subId: Int): ChallengeResponse {
|
||||
val expectedPayloads = verified_senders
|
||||
.map { it.phone_number_id.toByteArray() }
|
||||
.filter { it.isNotEmpty() }
|
||||
if (expectedPayloads.isEmpty()) return ChallengeResponse(
|
||||
registered_sms_response = RegisteredSmsChallengeResponse(
|
||||
items = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
val localNumbers = getLocalNumbers(context, subId)
|
||||
if (localNumbers.isEmpty()) {
|
||||
Log.w(TAG, "No local phone numbers available for registered SMS verification")
|
||||
return ChallengeResponse(registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList()))
|
||||
}
|
||||
|
||||
val historyStart = System.currentTimeMillis() -
|
||||
TimeUnit.HOURS.toMillis(VerificationSettingsPhenotypes.A2P_HISTORY_WINDOW_HOURS)
|
||||
val bucketSizeMillis = (TimeUnit.HOURS.toMillis(
|
||||
VerificationSettingsPhenotypes.A2P_SMS_SIGNAL_GRANULARITY_HRS
|
||||
) / 2).coerceAtLeast(1L)
|
||||
|
||||
val responseItems = mutableListOf<RegisteredSmsChallengeResponseItem>()
|
||||
|
||||
try {
|
||||
val cursor = context.contentResolver.query(
|
||||
Telephony.Sms.Inbox.CONTENT_URI,
|
||||
arrayOf("date", "address", "body", "sub_id"),
|
||||
"date > ?",
|
||||
arrayOf(historyStart.toString()),
|
||||
"date DESC"
|
||||
) ?: return ChallengeResponse(
|
||||
registered_sms_response = RegisteredSmsChallengeResponse(
|
||||
items = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
cursor.use {
|
||||
while (it.moveToNext()) {
|
||||
val date = it.getLong(it.getColumnIndexOrThrow("date"))
|
||||
val sender = it.getString(it.getColumnIndexOrThrow("address")) ?: continue
|
||||
val body = it.getString(it.getColumnIndexOrThrow("body")) ?: continue
|
||||
val messageSubId = runCatching {
|
||||
it.getInt(it.getColumnIndexOrThrow("sub_id"))
|
||||
}.getOrDefault(-1)
|
||||
|
||||
val candidateNumbers = if (subId != -1 && messageSubId == subId) {
|
||||
localNumbers
|
||||
} else if (messageSubId == -1) {
|
||||
localNumbers
|
||||
} else {
|
||||
getLocalNumbers(context, messageSubId).ifEmpty { localNumbers }
|
||||
}
|
||||
|
||||
val bucketStart = date - (date % bucketSizeMillis)
|
||||
for (localNumber in candidateNumbers) {
|
||||
val payload = computePayload(bucketStart, localNumber, sender, body)
|
||||
if (expectedPayloads.any { expected -> expected.contentEquals(payload) }) {
|
||||
responseItems += RegisteredSmsChallengeResponseItem(
|
||||
payload = RegisteredSmsChallengeResponsePayload(
|
||||
payload = payload.toByteString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Registered SMS verification failed", e)
|
||||
return ChallengeResponse(registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList()))
|
||||
}
|
||||
|
||||
return ChallengeResponse(
|
||||
registered_sms_response = RegisteredSmsChallengeResponse(items = responseItems.distinct())
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getLocalNumbers(context: Context, targetSubId: Int): List<String> {
|
||||
val numbers = linkedSetOf<String>()
|
||||
val subscriptionManager =
|
||||
context.getSystemService<SubscriptionManager>()
|
||||
val telephonyManager = context.getSystemService<TelephonyManager>()
|
||||
|
||||
val hasState = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_PHONE_STATE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
val hasNumbers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_PHONE_NUMBERS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
hasState
|
||||
}
|
||||
|
||||
if (telephonyManager?.hasCarrierPrivileges() != true && (!hasState || !hasNumbers)) {
|
||||
Log.e(TAG, "Permission not granted")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
try {
|
||||
subscriptionManager?.activeSubscriptionInfoList.orEmpty().forEach { info ->
|
||||
if (targetSubId != -1 && info.subscriptionId != targetSubId) return@forEach
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && subscriptionManager != null) {
|
||||
numbers += subscriptionManager.getPhoneNumber(
|
||||
info.subscriptionId,
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER
|
||||
)
|
||||
numbers += subscriptionManager.getPhoneNumber(
|
||||
info.subscriptionId,
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_UICC
|
||||
)
|
||||
numbers += subscriptionManager.getPhoneNumber(
|
||||
info.subscriptionId,
|
||||
SubscriptionManager.PHONE_NUMBER_SOURCE_IMS
|
||||
)
|
||||
}
|
||||
val targetManager =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
telephonyManager?.createForSubscriptionId(info.subscriptionId)
|
||||
} else {
|
||||
telephonyManager
|
||||
}
|
||||
numbers += targetManager?.line1Number.orEmpty()
|
||||
numbers += info.number.orEmpty()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to collect local phone numbers", e)
|
||||
}
|
||||
|
||||
return numbers.filter { it.isNotBlank() }.distinct()
|
||||
}
|
||||
|
||||
private fun computePayload(
|
||||
bucketStart: Long,
|
||||
localNumber: String,
|
||||
sender: String,
|
||||
body: String
|
||||
): ByteArray {
|
||||
val digest = MessageDigest.getInstance("SHA-512")
|
||||
digest.update(bucketStart.toString().toByteArray(StandardCharsets.UTF_8))
|
||||
digest.update(hashUtf8(localNumber))
|
||||
digest.update(hashUtf8(sender))
|
||||
digest.update(body.toByteArray(StandardCharsets.UTF_8))
|
||||
return digest.digest()
|
||||
}
|
||||
|
||||
private fun hashUtf8(value: String): ByteArray {
|
||||
return MessageDigest.getInstance("SHA-512")
|
||||
.digest(value.toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
@file:SuppressLint("HardwareIds")
|
||||
|
||||
package org.microg.gms.constellation.core.verification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
import org.microg.gms.constellation.core.proto.ChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.ClientChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.OdsaOperation
|
||||
import org.microg.gms.constellation.core.proto.ServerChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.ServiceEntitlementRequest
|
||||
import org.microg.gms.constellation.core.proto.Ts43Challenge
|
||||
import org.microg.gms.constellation.core.proto.Ts43ChallengeResponse
|
||||
import org.microg.gms.constellation.core.proto.Ts43ChallengeResponseError
|
||||
import org.microg.gms.constellation.core.proto.Ts43ChallengeResponseStatus
|
||||
import org.microg.gms.constellation.core.verification.ts43.EapAkaService
|
||||
import org.microg.gms.constellation.core.verification.ts43.builder
|
||||
import org.microg.gms.constellation.core.verification.ts43.userAgent
|
||||
import org.xml.sax.InputSource
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
private const val TAG = "Ts43Verifier"
|
||||
|
||||
fun Ts43Challenge.verify(context: Context, subId: Int): ChallengeResponse {
|
||||
val baseTelephonyManager = requireNotNull(context.getSystemService<TelephonyManager>()) {
|
||||
"TelephonyManager unavailable"
|
||||
}
|
||||
val telephonyManager = if (subId >= 0) {
|
||||
baseTelephonyManager.createForSubscriptionId(subId)
|
||||
} else {
|
||||
baseTelephonyManager
|
||||
}
|
||||
val eapAkaService = EapAkaService(telephonyManager)
|
||||
val okHttpClient = newTs43HttpClient()
|
||||
val httpHistory = mutableListOf<String>()
|
||||
val op: OdsaOperation?
|
||||
val requestType: Ts43ChallengeResponseError.RequestType
|
||||
val routeName: String
|
||||
val responseBuilder: (String, List<String>) -> Ts43ChallengeResponse
|
||||
|
||||
when {
|
||||
client_challenge != null -> {
|
||||
op = client_challenge.operation
|
||||
requestType =
|
||||
Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_GET_PHONE_NUMBER_API
|
||||
routeName = "Client"
|
||||
responseBuilder = { responseBody, history ->
|
||||
Ts43ChallengeResponse(
|
||||
ts43_type = ts43_type,
|
||||
client_challenge_response = ClientChallengeResponse(
|
||||
get_phone_number_response = responseBody
|
||||
),
|
||||
http_history = history
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
server_challenge != null -> {
|
||||
op = server_challenge.operation
|
||||
requestType =
|
||||
Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_ACQUIRE_TEMPORARY_TOKEN_API
|
||||
routeName = "Server"
|
||||
responseBuilder = { responseBody, history ->
|
||||
Ts43ChallengeResponse(
|
||||
ts43_type = ts43_type,
|
||||
server_challenge_response = ServerChallengeResponse(
|
||||
acquire_temporary_token_response = responseBody
|
||||
),
|
||||
http_history = history
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
return failureResponse(
|
||||
this,
|
||||
Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET,
|
||||
httpHistory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
Log.d(TAG, "TS43: $routeName ODSA challenge route: ${op?.operation}")
|
||||
if (op == null) {
|
||||
return failureResponse(
|
||||
this,
|
||||
Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET,
|
||||
httpHistory
|
||||
)
|
||||
}
|
||||
|
||||
val requestPayload = buildOdsaRequestPayload(
|
||||
context,
|
||||
telephonyManager,
|
||||
eapAkaService,
|
||||
okHttpClient,
|
||||
httpHistory,
|
||||
entitlement_url,
|
||||
eap_aka_realm,
|
||||
service_entitlement_request,
|
||||
app_id
|
||||
) ?: return failureResponse(
|
||||
this,
|
||||
Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR,
|
||||
httpHistory
|
||||
)
|
||||
|
||||
val responseBody = performOdsaRequest(
|
||||
context,
|
||||
telephonyManager,
|
||||
okHttpClient,
|
||||
httpHistory,
|
||||
entitlement_url,
|
||||
op,
|
||||
requestPayload,
|
||||
app_id,
|
||||
requestType
|
||||
)
|
||||
ChallengeResponse(
|
||||
ts43_challenge_response = responseBuilder(responseBody, httpHistory.toList())
|
||||
)
|
||||
} catch (e: Ts43ApiException) {
|
||||
Log.w(TAG, "TS43 API failure requestType=${e.requestType} http=${e.httpStatus}", e)
|
||||
ChallengeResponse(
|
||||
ts43_challenge_response = Ts43ChallengeResponse(
|
||||
ts43_type = ts43_type,
|
||||
status = Ts43ChallengeResponseStatus(
|
||||
error = Ts43ChallengeResponseError(
|
||||
error_code = e.errorCode,
|
||||
http_status = e.httpStatus,
|
||||
request_type = e.requestType
|
||||
)
|
||||
),
|
||||
http_history = httpHistory.toList()
|
||||
)
|
||||
)
|
||||
} catch (e: NullPointerException) {
|
||||
Log.e(TAG, "TS43 null failure", e)
|
||||
failureResponse(
|
||||
this,
|
||||
Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR,
|
||||
httpHistory
|
||||
)
|
||||
} catch (e: RuntimeException) {
|
||||
Log.e(TAG, "TS43 runtime failure", e)
|
||||
failureResponse(
|
||||
this,
|
||||
Ts43ChallengeResponseStatus.Code.TS43_STATUS_RUNTIME_ERROR,
|
||||
httpHistory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performOdsaRequest(
|
||||
context: Context,
|
||||
telephonyManager: TelephonyManager,
|
||||
okHttpClient: OkHttpClient,
|
||||
httpHistory: MutableList<String>,
|
||||
entitlementUrl: String,
|
||||
op: OdsaOperation,
|
||||
req: ServiceEntitlementRequest?,
|
||||
challengeAppId: String?,
|
||||
requestType: Ts43ChallengeResponseError.RequestType
|
||||
): String {
|
||||
val appIds = listOfNotNull(challengeAppId?.takeIf { it.isNotBlank() })
|
||||
.ifEmpty { listOf("ap2014") }
|
||||
val builder = op.builder(telephonyManager, req, appIds)
|
||||
val url = builder.buildBaseUrl(entitlementUrl)
|
||||
val userAgent = req?.userAgent(context) ?: "PRD-TS43 OS-Android/${Build.VERSION.RELEASE}"
|
||||
val accept = req?.accept_content_type?.takeIf { it.isNotEmpty() } ?: "application/json"
|
||||
val acceptLanguage = Locale.getDefault().toLanguageTag().ifEmpty { "en-US" }
|
||||
httpHistory += "GET $url"
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.header("Accept", accept)
|
||||
.header("User-Agent", userAgent)
|
||||
.header("Accept-Language", acceptLanguage)
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
httpHistory += "RESP ${response.code} ${request.url}"
|
||||
if (response.isSuccessful) {
|
||||
response.body?.string()
|
||||
?: throw Ts43ApiException(
|
||||
errorCode = errorCode(32),
|
||||
httpStatus = response.code,
|
||||
requestType = requestType
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "ODSA request failed: ${response.code} for ${op.operation}")
|
||||
throw Ts43ApiException(
|
||||
errorCode = errorCode(31),
|
||||
httpStatus = response.code,
|
||||
requestType = requestType
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, " Network error in ODSA request", e)
|
||||
throw Ts43ApiException(
|
||||
errorCode = errorCode(30),
|
||||
httpStatus = -1,
|
||||
requestType = requestType,
|
||||
cause = e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOdsaRequestPayload(
|
||||
context: Context,
|
||||
telephonyManager: TelephonyManager,
|
||||
eapAkaService: EapAkaService,
|
||||
okHttpClient: OkHttpClient,
|
||||
httpHistory: MutableList<String>,
|
||||
entitlementUrl: String,
|
||||
eapAkaRealm: String?,
|
||||
req: ServiceEntitlementRequest?,
|
||||
challengeAppId: String?
|
||||
): ServiceEntitlementRequest? {
|
||||
if (req == null) return null
|
||||
if (req.authentication_token.isNotEmpty() || req.temporary_token.isNotEmpty()) return req
|
||||
|
||||
val mccMnc = telephonyManager.simOperator ?: ""
|
||||
val imsi = telephonyManager.subscriberId ?: ""
|
||||
if (mccMnc.length < 5 || imsi.isEmpty()) return null
|
||||
|
||||
val eapId = eapAkaService.buildEapId(mccMnc, imsi, eapAkaRealm)
|
||||
val appIds = listOfNotNull(challengeAppId?.takeIf { it.isNotBlank() })
|
||||
.ifEmpty { listOf("ap2014") }
|
||||
val builder = req.builder(telephonyManager, eapId, appIds)
|
||||
|
||||
val initialUrl = builder.buildBaseUrl(entitlementUrl)
|
||||
val postUrl = entitlementUrl
|
||||
val userAgent = req.userAgent(context)
|
||||
val acceptLanguage = Locale.getDefault().toLanguageTag().ifEmpty { "en-US" }
|
||||
|
||||
var currentRequest = Request.Builder()
|
||||
.url(initialUrl)
|
||||
.header("Accept", "application/vnd.gsma.eap-relay.v1.0+json")
|
||||
.header("User-Agent", userAgent)
|
||||
.header("Accept-Language", acceptLanguage)
|
||||
.build()
|
||||
httpHistory += "GET $initialUrl"
|
||||
|
||||
for (round in 1..3) {
|
||||
val response = try {
|
||||
okHttpClient.newCall(currentRequest).execute()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Network error in EAP round $round", e)
|
||||
throw Ts43ApiException(
|
||||
errorCode = errorCode(30),
|
||||
httpStatus = -1,
|
||||
requestType = Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_AUTH_API,
|
||||
cause = e
|
||||
)
|
||||
}
|
||||
|
||||
val body = response.body?.string() ?: return null
|
||||
httpHistory += "RESP ${response.code} ${currentRequest.url}"
|
||||
if (!response.isSuccessful) {
|
||||
Log.w(TAG, "EAP round $round failed with code ${response.code}")
|
||||
throw Ts43ApiException(
|
||||
errorCode = errorCode(31),
|
||||
httpStatus = response.code,
|
||||
requestType = Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_AUTH_API
|
||||
)
|
||||
}
|
||||
|
||||
val token = extractAuthToken(body)
|
||||
if (token != null) {
|
||||
return req.copy(authentication_token = token)
|
||||
}
|
||||
|
||||
val eapRelayPacket = extractEapRelayPacket(body)
|
||||
?: throw Ts43ApiException(
|
||||
errorCode = errorCode(32),
|
||||
httpStatus = response.code,
|
||||
requestType = Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_AUTH_API
|
||||
)
|
||||
|
||||
val akaResponse = eapAkaService.performSimAkaAuth(eapRelayPacket, imsi, mccMnc)
|
||||
?: return null
|
||||
|
||||
val postBody = JSONObject().put("eap-relay-packet", akaResponse).toString()
|
||||
currentRequest = Request.Builder()
|
||||
.url(postUrl)
|
||||
.header(
|
||||
"Accept",
|
||||
"application/vnd.gsma.eap-relay.v1.0+json, text/vnd.wap.connectivity-xml"
|
||||
)
|
||||
.header("User-Agent", userAgent)
|
||||
.header("Content-Type", "application/vnd.gsma.eap-relay.v1.0+json")
|
||||
.header("Accept-Language", acceptLanguage)
|
||||
.post(
|
||||
postBody.toByteArray().toRequestBody(
|
||||
"application/vnd.gsma.eap-relay.v1.0+json".toMediaType()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
httpHistory += "POST $postUrl"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun extractEapRelayPacket(body: String): String? {
|
||||
return runCatching { JSONObject(body).optString("eap-relay-packet") }
|
||||
.getOrNull()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun extractAuthToken(body: String): String? {
|
||||
runCatching {
|
||||
JSONObject(body).optJSONObject("Token")?.optString("token")
|
||||
}.getOrNull()?.takeIf { it.isNotEmpty() }?.let { return it }
|
||||
|
||||
return runCatching {
|
||||
val normalized = body
|
||||
.replace("&", "&")
|
||||
.replace("&amp;", "&")
|
||||
.replace("\r\n", "\n")
|
||||
val doc = DocumentBuilderFactory.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.parse(InputSource(StringReader(normalized)))
|
||||
val parms = doc.getElementsByTagName("parm")
|
||||
for (i in 0 until parms.length) {
|
||||
val node = parms.item(i)
|
||||
val attrs = node.attributes ?: continue
|
||||
val name = attrs.getNamedItem("name")?.nodeValue ?: continue
|
||||
if (name == "token") {
|
||||
return@runCatching attrs.getNamedItem("value")?.nodeValue
|
||||
}
|
||||
}
|
||||
null
|
||||
}.getOrNull()?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun failureResponse(
|
||||
challenge: Ts43Challenge,
|
||||
statusCode: Ts43ChallengeResponseStatus.Code,
|
||||
httpHistory: List<String>
|
||||
): ChallengeResponse {
|
||||
return ChallengeResponse(
|
||||
ts43_challenge_response = Ts43ChallengeResponse(
|
||||
ts43_type = challenge.ts43_type,
|
||||
status = Ts43ChallengeResponseStatus(status_code = statusCode),
|
||||
http_history = httpHistory.toList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private class Ts43ApiException(
|
||||
val errorCode: Ts43ChallengeResponseError.Code,
|
||||
val httpStatus: Int,
|
||||
val requestType: Ts43ChallengeResponseError.RequestType,
|
||||
cause: Throwable? = null
|
||||
) : Exception(cause)
|
||||
|
||||
private fun errorCode(rawCode: Int): Ts43ChallengeResponseError.Code =
|
||||
Ts43ChallengeResponseError.Code.fromValue(rawCode)
|
||||
?: Ts43ChallengeResponseError.Code.TS43_ERROR_CODE_UNSPECIFIED
|
||||
|
||||
private fun newTs43HttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(false)
|
||||
.cookieJar(object : CookieJar {
|
||||
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val existing = cookieStore.getOrPut(url.host) { mutableListOf() }
|
||||
for (cookie in cookies) {
|
||||
existing.removeAll {
|
||||
it.name == cookie.name &&
|
||||
it.domain == cookie.domain &&
|
||||
it.path == cookie.path
|
||||
}
|
||||
existing += cookie
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return cookieStore[url.host]
|
||||
?.filter { cookie -> cookie.matches(url) }
|
||||
.orEmpty()
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package org.microg.gms.constellation.core.verification.ts43
|
||||
|
||||
import android.os.Build
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
private const val TAG = "EapAkaService"
|
||||
|
||||
class EapAkaService(private val telephonyManager: TelephonyManager) {
|
||||
|
||||
companion object {
|
||||
private const val EAP_CODE_REQUEST = 1
|
||||
private const val EAP_CODE_RESPONSE = 2
|
||||
|
||||
private const val EAP_TYPE_AKA = 23
|
||||
|
||||
private const val EAP_AKA_SUBTYPE_CHALLENGE = 1
|
||||
private const val EAP_AKA_SUBTYPE_SYNC_FAILURE = 4
|
||||
|
||||
private const val AT_RAND = 1
|
||||
private const val AT_AUTN = 2
|
||||
private const val AT_RES = 3
|
||||
private const val AT_AUTS = 4
|
||||
private const val AT_MAC = 11
|
||||
|
||||
private const val SIM_RES_SUCCESS = 0xDB.toByte()
|
||||
private const val SIM_RES_SYNC_FAIL = 0xDC.toByte()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun performSimAkaAuth(eapRelayBase64: String, imsi: String, mccMnc: String): String? {
|
||||
val eapPacket = Base64.decode(eapRelayBase64, Base64.DEFAULT)
|
||||
if (eapPacket.size < 12) return null
|
||||
|
||||
val code = eapPacket[0].toInt()
|
||||
val eapId = eapPacket[1]
|
||||
val type = eapPacket[4].toInt()
|
||||
val subtype = eapPacket[5].toInt()
|
||||
|
||||
if (code != EAP_CODE_REQUEST || type != EAP_TYPE_AKA || subtype != EAP_AKA_SUBTYPE_CHALLENGE) {
|
||||
Log.w(TAG, "Unexpected EAP packet: code=$code, type=$type, subtype=$subtype")
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse attributes (starting at offset 8)
|
||||
var rand: ByteArray? = null
|
||||
var autn: ByteArray? = null
|
||||
|
||||
var offset = 8
|
||||
while (offset + 2 <= eapPacket.size) {
|
||||
val attrType = eapPacket[offset].toInt() and 0xFF
|
||||
val attrLen = (eapPacket[offset + 1].toInt() and 0xFF) * 4
|
||||
if (offset + attrLen > eapPacket.size || attrLen < 4) break
|
||||
|
||||
when (attrType) {
|
||||
AT_RAND -> {
|
||||
if (attrLen >= 20) {
|
||||
rand = ByteArray(16)
|
||||
System.arraycopy(eapPacket, offset + 4, rand, 0, 16)
|
||||
}
|
||||
}
|
||||
|
||||
AT_AUTN -> {
|
||||
if (attrLen >= 20) {
|
||||
autn = ByteArray(16)
|
||||
System.arraycopy(eapPacket, offset + 4, autn, 0, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += attrLen
|
||||
if (rand != null && autn != null) break
|
||||
}
|
||||
|
||||
if (rand == null || autn == null) {
|
||||
Log.e(TAG, "Missing RAND or AUTN in EAP-AKA challenge")
|
||||
return null
|
||||
}
|
||||
|
||||
val challengeBytes = byteArrayOf(16) + rand + byteArrayOf(16) + autn
|
||||
val challengeB64 = Base64.encodeToString(challengeBytes, Base64.NO_WRAP)
|
||||
|
||||
val iccAuthResult = telephonyManager.getIccAuthentication(
|
||||
TelephonyManager.APPTYPE_USIM, TelephonyManager.AUTHTYPE_EAP_AKA, challengeB64
|
||||
) ?: run {
|
||||
Log.e(TAG, "SIM returned null for AKA auth")
|
||||
return null
|
||||
}
|
||||
|
||||
val iccBytes = Base64.decode(iccAuthResult, Base64.DEFAULT)
|
||||
if (iccBytes.isEmpty()) return null
|
||||
|
||||
return when (iccBytes[0]) {
|
||||
SIM_RES_SUCCESS -> {
|
||||
val res = extractTlv(1, iccBytes) ?: return null
|
||||
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)
|
||||
|
||||
val kAut = keys["K_aut"] ?: run {
|
||||
Log.e(TAG, "Failed to derive K_aut")
|
||||
return null
|
||||
}
|
||||
|
||||
val responsePacket = buildEapAkaResponse(eapId, res, kAut) ?: return null
|
||||
Base64.encodeToString(responsePacket, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
SIM_RES_SYNC_FAIL -> {
|
||||
val auts = extractTlv(1, iccBytes) ?: return null
|
||||
val responsePacket = buildEapAkaSyncFailure(eapId, auts)
|
||||
Base64.encodeToString(responsePacket, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.e(TAG, "Unknown SIM response tag: ${iccBytes[0]}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
private fun extractTlv(index: Int, data: ByteArray): ByteArray? {
|
||||
if (index >= data.size) return null
|
||||
val len = data[index].toInt() and 0xFF
|
||||
if (index + 1 + len > data.size) return null
|
||||
return data.copyOfRange(index + 1, index + 1 + len)
|
||||
}
|
||||
|
||||
private fun buildEapAkaResponse(id: Byte, res: ByteArray, kAut: ByteArray): ByteArray? {
|
||||
val resAttrLen = ((res.size + 4 + 3) / 4) * 4
|
||||
val totalLen = 8 + resAttrLen + 20
|
||||
val buffer = ByteBuffer.allocate(totalLen)
|
||||
|
||||
buffer.put(EAP_CODE_RESPONSE.toByte())
|
||||
buffer.put(id)
|
||||
buffer.putShort(totalLen.toShort())
|
||||
buffer.put(EAP_TYPE_AKA.toByte())
|
||||
buffer.put(EAP_AKA_SUBTYPE_CHALLENGE.toByte())
|
||||
buffer.putShort(0)
|
||||
|
||||
buffer.put(AT_RES.toByte())
|
||||
buffer.put((resAttrLen / 4).toByte())
|
||||
buffer.putShort((res.size * 8).toShort())
|
||||
buffer.put(res)
|
||||
|
||||
val padding = resAttrLen - 4 - res.size
|
||||
if (padding > 0) buffer.put(ByteArray(padding))
|
||||
|
||||
buffer.position()
|
||||
buffer.put(AT_MAC.toByte())
|
||||
buffer.put(5)
|
||||
buffer.putShort(0)
|
||||
val macValueOffset = buffer.position()
|
||||
buffer.put(ByteArray(16))
|
||||
|
||||
val packet = buffer.array()
|
||||
val mac = hmacSha1(kAut, packet) ?: return null
|
||||
|
||||
System.arraycopy(mac, 0, packet, macValueOffset, 16)
|
||||
return packet
|
||||
}
|
||||
|
||||
private fun buildEapAkaSyncFailure(id: Byte, auts: ByteArray): ByteArray {
|
||||
val attrLen = ((auts.size + 2 + 3) / 4) * 4
|
||||
val totalLen = 8 + attrLen
|
||||
|
||||
return ByteBuffer.allocate(totalLen).apply {
|
||||
put(EAP_CODE_RESPONSE.toByte())
|
||||
put(id)
|
||||
putShort(totalLen.toShort())
|
||||
put(EAP_TYPE_AKA.toByte())
|
||||
put(EAP_AKA_SUBTYPE_SYNC_FAILURE.toByte())
|
||||
putShort(0)
|
||||
|
||||
put(AT_AUTS.toByte())
|
||||
put((attrLen / 4).toByte())
|
||||
put(auts)
|
||||
|
||||
val padding = attrLen - 2 - auts.size
|
||||
if (padding > 0) put(ByteArray(padding))
|
||||
}.array()
|
||||
}
|
||||
|
||||
private fun hmacSha1(key: ByteArray, data: ByteArray): ByteArray? = try {
|
||||
Mac.getInstance("HmacSHA1").apply { init(SecretKeySpec(key, "HmacSHA1")) }.doFinal(data)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package org.microg.gms.constellation.core.verification.ts43
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
object Fips186Prf {
|
||||
fun deriveKeys(
|
||||
identityBytes: ByteArray,
|
||||
ik: ByteArray,
|
||||
ck: ByteArray
|
||||
): Map<String, ByteArray> {
|
||||
val xKey = try {
|
||||
val md = MessageDigest.getInstance("SHA-1")
|
||||
md.update(identityBytes)
|
||||
md.update(ik)
|
||||
md.update(ck)
|
||||
md.digest()
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
ByteArray(20)
|
||||
}
|
||||
|
||||
if (xKey.size != 20) return emptyMap()
|
||||
|
||||
val result = ByteArray(160)
|
||||
val xKeyWorking = xKey.copyOf()
|
||||
var resultOffset = 0
|
||||
|
||||
repeat(8) {
|
||||
val h = intArrayOf(
|
||||
0x67452301,
|
||||
0xEFCDAB89.toInt(),
|
||||
0x98BADCFE.toInt(),
|
||||
0x10325476,
|
||||
0xC3D2E1F0.toInt()
|
||||
)
|
||||
val w = IntArray(80)
|
||||
for (k in 0 until 16) {
|
||||
val wordIdx = k * 4
|
||||
w[k] = if (wordIdx < 20) {
|
||||
((xKeyWorking.getOrElse(wordIdx) { 0 }.toInt() and 0xFF) shl 24) or
|
||||
((xKeyWorking.getOrElse(wordIdx + 1) { 0 }
|
||||
.toInt() and 0xFF) shl 16) or
|
||||
((xKeyWorking.getOrElse(wordIdx + 2) { 0 }
|
||||
.toInt() and 0xFF) shl 8) or
|
||||
(xKeyWorking.getOrElse(wordIdx + 3) { 0 }.toInt() and 0xFF)
|
||||
} else 0
|
||||
}
|
||||
for (k in 16 until 80) {
|
||||
val temp = w[k - 3] xor w[k - 8] xor w[k - 14] xor w[k - 16]
|
||||
w[k] = (temp shl 1) or (temp ushr 31)
|
||||
}
|
||||
var a = h[0]
|
||||
var b = h[1]
|
||||
var c = h[2]
|
||||
var d = h[3]
|
||||
var e = h[4]
|
||||
for (t in 0 until 80) {
|
||||
val f: Int
|
||||
val k: Int
|
||||
when {
|
||||
t <= 19 -> {
|
||||
f = (b and c) or (b.inv() and d); k = 0x5A827999
|
||||
}
|
||||
|
||||
t <= 39 -> {
|
||||
f = b xor c xor d; k = 0x6ED9EBA1
|
||||
}
|
||||
|
||||
t <= 59 -> {
|
||||
f = (b and c) or (b and d) or (c and d); k = 0x8F1BBCDC.toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
f = b xor c xor d; k = 0xCA62C1D6.toInt()
|
||||
}
|
||||
}
|
||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + k + w[t]
|
||||
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
|
||||
}
|
||||
val block = IntArray(5)
|
||||
block[0] = h[0] + a; block[1] = h[1] + b; block[2] = h[2] + c; block[3] =
|
||||
h[3] + d; block[4] = h[4] + e
|
||||
for (k in 0 until 5) {
|
||||
val word = block[k]
|
||||
result[resultOffset++] = (word shr 24).toByte()
|
||||
result[resultOffset++] = (word shr 16).toByte()
|
||||
result[resultOffset++] = (word shr 8).toByte()
|
||||
result[resultOffset++] = word.toByte()
|
||||
}
|
||||
var carry = 1
|
||||
for (k in 19 downTo 0) {
|
||||
val resByte = result[resultOffset - 20 + k].toInt() and 0xFF
|
||||
val keyByte = xKeyWorking[k].toInt() and 0xFF
|
||||
val sum = carry + keyByte + resByte
|
||||
xKeyWorking[k] = sum.toByte()
|
||||
carry = sum shr 8
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 4187 Section 7: PRF output slicing
|
||||
return mapOf(
|
||||
"K_encr" to result.copyOfRange(0, 16),
|
||||
"K_aut" to result.copyOfRange(16, 32),
|
||||
"MSK" to result.copyOfRange(32, 96),
|
||||
"EMSK" to result.copyOfRange(96, 160)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
@file:RequiresApi(Build.VERSION_CODES.O)
|
||||
@file:SuppressLint("HardwareIds", "MissingPermission")
|
||||
|
||||
package org.microg.gms.constellation.core.verification.ts43
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.microg.gms.constellation.core.proto.OdsaOperation
|
||||
import org.microg.gms.constellation.core.proto.ServiceEntitlementRequest
|
||||
|
||||
fun ServiceEntitlementRequest.builder(
|
||||
telephonyManager: TelephonyManager,
|
||||
eapId: String,
|
||||
appIds: List<String> = emptyList()
|
||||
) = ServiceEntitlementBuilder(
|
||||
imsi = telephonyManager.subscriberId ?: "",
|
||||
iccid = try {
|
||||
telephonyManager.simSerialNumber
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
},
|
||||
terminalId = telephonyManager.imei,
|
||||
groupIdLevel1 = runCatching { telephonyManager.groupIdLevel1 }.getOrNull(),
|
||||
eapId = eapId,
|
||||
appIds = appIds,
|
||||
req = this
|
||||
)
|
||||
|
||||
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)
|
||||
return "PRD-TS43 term-$vendor/$model /$packageVersion OS-Android/$swVersion"
|
||||
}
|
||||
|
||||
fun OdsaOperation.builder(
|
||||
telephonyManager: TelephonyManager,
|
||||
serviceEntitlementRequest: ServiceEntitlementRequest? = null,
|
||||
appIds: List<String> = emptyList()
|
||||
) = ServiceEntitlementBuilder(
|
||||
imsi = telephonyManager.subscriberId ?: "",
|
||||
iccid = try {
|
||||
telephonyManager.simSerialNumber
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
},
|
||||
terminalId = telephonyManager.imei,
|
||||
groupIdLevel1 = runCatching { telephonyManager.groupIdLevel1 }.getOrNull(),
|
||||
eapId = "", // Not needed for ODSA
|
||||
appIds = appIds,
|
||||
req = serviceEntitlementRequest ?: ServiceEntitlementRequest(),
|
||||
odsa = this
|
||||
)
|
||||
|
||||
class ServiceEntitlementBuilder(
|
||||
private val imsi: String,
|
||||
private val iccid: String?,
|
||||
private val terminalId: String?,
|
||||
private val groupIdLevel1: String?,
|
||||
private val eapId: String,
|
||||
private val appIds: List<String>,
|
||||
private val req: ServiceEntitlementRequest,
|
||||
private val odsa: OdsaOperation? = null
|
||||
) {
|
||||
fun buildBaseUrl(entitlementUrl: String): HttpUrl {
|
||||
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)
|
||||
|
||||
return baseUrl.newBuilder().apply {
|
||||
when {
|
||||
req.authentication_token.isNotEmpty() -> {
|
||||
addQueryParameter("token", req.authentication_token)
|
||||
if (imsi.isNotEmpty()) addQueryParameter("IMSI", imsi)
|
||||
}
|
||||
|
||||
req.temporary_token.isNotEmpty() -> {
|
||||
addQueryParameter("temporary_token", req.temporary_token)
|
||||
}
|
||||
|
||||
eapId.isNotEmpty() -> {
|
||||
addQueryParameter("EAP_ID", eapId)
|
||||
}
|
||||
}
|
||||
addQueryParameter("terminal_id", terminalId ?: req.terminal_id)
|
||||
if (req.gid1.isNotEmpty()) {
|
||||
addQueryParameter("GID1", req.gid1)
|
||||
} else if ((req.entitlement_version.toBigDecimalOrNull()?.toInt() ?: 0) >= 12) {
|
||||
groupIdLevel1?.takeIf { it.isNotEmpty() }?.let { addQueryParameter("GID1", it) }
|
||||
}
|
||||
if (req.app_version.isNotEmpty()) addQueryParameter("app_version", req.app_version)
|
||||
addQueryParameter("terminal_vendor", vendor)
|
||||
addQueryParameter("terminal_model", model)
|
||||
addQueryParameter("terminal_sw_version", swVersion)
|
||||
addQueryParameter("app_name", req.app_name)
|
||||
if (req.boost_type.isNotEmpty()) addQueryParameter("boost_type", req.boost_type)
|
||||
if (appIds.isNotEmpty()) {
|
||||
appIds.forEach { addQueryParameter("app", it) }
|
||||
} else {
|
||||
addQueryParameter("app", "ap2014")
|
||||
}
|
||||
addQueryParameter("vers", req.configuration_version.toString())
|
||||
addQueryParameter("entitlement_version", req.entitlement_version)
|
||||
if (req.notification_token.isNotEmpty()) {
|
||||
addQueryParameter("notif_action", req.notification_action.toString())
|
||||
addQueryParameter("notif_token", req.notification_token)
|
||||
}
|
||||
|
||||
// Handle ODSA specific fields if present
|
||||
odsa?.let {
|
||||
addQueryParameter("operation", it.operation)
|
||||
if (it.operation_type != -1) {
|
||||
addQueryParameter("operation_type", it.operation_type.toString())
|
||||
}
|
||||
if (it.operation_targets.isNotEmpty()) {
|
||||
addQueryParameter("operation_targets", it.operation_targets.joinToString(","))
|
||||
}
|
||||
if (it.terminal_iccid.isNotEmpty()) addQueryParameter(
|
||||
"terminal_iccid",
|
||||
it.terminal_iccid
|
||||
)
|
||||
else iccid?.let { i -> addQueryParameter("terminal_iccid", i) }
|
||||
|
||||
if (it.terminal_eid.isNotEmpty()) addQueryParameter("terminal_eid", it.terminal_eid)
|
||||
if (it.target_terminal_id.isNotEmpty()) addQueryParameter(
|
||||
"target_terminal_id",
|
||||
it.target_terminal_id
|
||||
)
|
||||
if (it.target_terminal_iccid.isNotEmpty()) addQueryParameter(
|
||||
"target_terminal_iccid",
|
||||
it.target_terminal_iccid
|
||||
)
|
||||
if (it.target_terminal_eid.isNotEmpty()) addQueryParameter(
|
||||
"target_terminal_eid",
|
||||
it.target_terminal_eid
|
||||
)
|
||||
if (it.target_terminal_model.isNotEmpty()) addQueryParameter(
|
||||
"target_terminal_model",
|
||||
it.target_terminal_model
|
||||
)
|
||||
if (it.target_terminal_serial_number.isNotEmpty()) addQueryParameter(
|
||||
"target_terminal_sn",
|
||||
it.target_terminal_serial_number
|
||||
)
|
||||
|
||||
it.target_terminal_ids.forEach { id ->
|
||||
addQueryParameter("target_terminal_imeis", id)
|
||||
}
|
||||
|
||||
if (it.old_terminal_id.isNotEmpty()) addQueryParameter(
|
||||
"old_terminal_id",
|
||||
it.old_terminal_id
|
||||
)
|
||||
if (it.old_terminal_iccid.isNotEmpty()) addQueryParameter(
|
||||
"old_terminal_iccid",
|
||||
it.old_terminal_iccid
|
||||
)
|
||||
|
||||
// Companion fields
|
||||
if (it.companion_terminal_id.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_id",
|
||||
it.companion_terminal_id
|
||||
)
|
||||
if (it.companion_terminal_vendor.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_vendor",
|
||||
it.companion_terminal_vendor
|
||||
)
|
||||
if (it.companion_terminal_model.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_model",
|
||||
it.companion_terminal_model
|
||||
)
|
||||
if (it.companion_terminal_software_version.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_sw_version",
|
||||
it.companion_terminal_software_version
|
||||
)
|
||||
if (it.companion_terminal_friendly_name.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_friendly_name",
|
||||
it.companion_terminal_friendly_name
|
||||
)
|
||||
if (it.companion_terminal_service.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_service",
|
||||
it.companion_terminal_service
|
||||
)
|
||||
if (it.companion_terminal_iccid.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_iccid",
|
||||
it.companion_terminal_iccid
|
||||
)
|
||||
if (it.companion_terminal_eid.isNotEmpty()) addQueryParameter(
|
||||
"companion_terminal_eid",
|
||||
it.companion_terminal_eid
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
1080
play-services-constellation/core/src/main/proto/constellation.proto
Normal file
1080
play-services-constellation/core/src/main/proto/constellation.proto
Normal file
File diff suppressed because it is too large
Load diff
6
play-services-constellation/src/main/AndroidManifest.xml
Normal file
6
play-services-constellation/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ SPDX-FileCopyrightText: 2026, microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest />
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
parcelable GetIidTokenRequest;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
parcelable GetIidTokenResponse;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
parcelable GetPnvCapabilitiesRequest;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
parcelable GetPnvCapabilitiesResponse;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
parcelable PhoneNumberInfo;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
parcelable VerifyPhoneNumberRequest;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
parcelable VerifyPhoneNumberResponse;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.google.android.gms.constellation.internal;
|
||||
|
||||
import com.google.android.gms.common.api.ApiMetadata;
|
||||
import com.google.android.gms.constellation.internal.IConstellationCallbacks;
|
||||
import com.google.android.gms.constellation.GetIidTokenRequest;
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesRequest;
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberRequest;
|
||||
|
||||
interface IConstellationApiService {
|
||||
void verifyPhoneNumberV1(
|
||||
IConstellationCallbacks cb,
|
||||
in Bundle bundle,
|
||||
in ApiMetadata apiMetadata
|
||||
);
|
||||
|
||||
void verifyPhoneNumberSingleUse(
|
||||
IConstellationCallbacks cb,
|
||||
in Bundle bundle,
|
||||
in ApiMetadata apiMetadata
|
||||
);
|
||||
|
||||
void verifyPhoneNumber(
|
||||
IConstellationCallbacks cb,
|
||||
in VerifyPhoneNumberRequest request,
|
||||
in ApiMetadata apiMetadata
|
||||
);
|
||||
|
||||
void getIidToken(
|
||||
IConstellationCallbacks cb,
|
||||
in GetIidTokenRequest request,
|
||||
in ApiMetadata apiMetadata
|
||||
);
|
||||
|
||||
void getPnvCapabilities(
|
||||
IConstellationCallbacks cb,
|
||||
in GetPnvCapabilitiesRequest request,
|
||||
in ApiMetadata apiMetadata
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.google.android.gms.constellation.internal;
|
||||
|
||||
import com.google.android.gms.common.api.ApiMetadata;
|
||||
import com.google.android.gms.common.api.Status;
|
||||
import com.google.android.gms.constellation.GetIidTokenResponse;
|
||||
import com.google.android.gms.constellation.GetPnvCapabilitiesResponse;
|
||||
import com.google.android.gms.constellation.PhoneNumberInfo;
|
||||
import com.google.android.gms.constellation.VerifyPhoneNumberResponse;
|
||||
|
||||
oneway interface IConstellationCallbacks {
|
||||
void onPhoneNumberVerified(in Status status, in List<PhoneNumberInfo> phoneNumbers, in ApiMetadata apiMetadata);
|
||||
void onPhoneNumberVerificationsCompleted(in Status status, in VerifyPhoneNumberResponse response, in ApiMetadata apiMetadata);
|
||||
void onIidTokenGenerated(in Status status, in GetIidTokenResponse response, in ApiMetadata apiMetadata);
|
||||
void onGetPnvCapabilitiesCompleted(in Status status, in GetPnvCapabilitiesResponse response, in ApiMetadata apiMetadata);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class GetIidTokenRequest extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<GetIidTokenRequest> CREATOR =
|
||||
findCreator(GetIidTokenRequest.class);
|
||||
|
||||
@Field(1)
|
||||
@Nullable
|
||||
public final Long projectNumber;
|
||||
|
||||
@Constructor
|
||||
public GetIidTokenRequest(
|
||||
@Param(1) @Nullable Long projectNumber
|
||||
) {
|
||||
this.projectNumber = projectNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class GetIidTokenResponse extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<GetIidTokenResponse> CREATOR =
|
||||
findCreator(GetIidTokenResponse.class);
|
||||
|
||||
@Field(1)
|
||||
public final String iidToken;
|
||||
|
||||
@Field(2)
|
||||
public final String fid;
|
||||
|
||||
@Field(3)
|
||||
@Nullable
|
||||
public final byte[] signature;
|
||||
|
||||
@Field(4)
|
||||
public final long timestamp;
|
||||
|
||||
@Constructor
|
||||
public GetIidTokenResponse(
|
||||
@Param(1) String iidToken,
|
||||
@Param(2) String fid,
|
||||
@Param(3) @Nullable byte[] signature,
|
||||
@Param(4) long timestamp
|
||||
) {
|
||||
this.iidToken = iidToken;
|
||||
this.fid = fid;
|
||||
this.signature = signature;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class GetPnvCapabilitiesRequest extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<GetPnvCapabilitiesRequest> CREATOR =
|
||||
findCreator(GetPnvCapabilitiesRequest.class);
|
||||
|
||||
@Field(1)
|
||||
public final String policyId;
|
||||
|
||||
@Field(2)
|
||||
public final List<Integer> verificationTypes;
|
||||
|
||||
@Field(3)
|
||||
public final List<Integer> simSlotIndices;
|
||||
|
||||
@Constructor
|
||||
public GetPnvCapabilitiesRequest(
|
||||
@Param(1) String policyId,
|
||||
@Param(2) List<Integer> verificationTypes,
|
||||
@Param(3) List<Integer> simSlotIndices
|
||||
) {
|
||||
this.policyId = policyId;
|
||||
this.verificationTypes = verificationTypes;
|
||||
this.simSlotIndices = simSlotIndices;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class GetPnvCapabilitiesResponse extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<GetPnvCapabilitiesResponse> CREATOR =
|
||||
findCreator(GetPnvCapabilitiesResponse.class);
|
||||
|
||||
@Field(1)
|
||||
public final List<SimCapability> simCapabilities;
|
||||
|
||||
@Constructor
|
||||
public GetPnvCapabilitiesResponse(
|
||||
@Param(1) List<SimCapability> simCapabilities
|
||||
) {
|
||||
this.simCapabilities = simCapabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
|
||||
@SafeParcelable.Class
|
||||
public static class SimCapability extends AbstractSafeParcelable {
|
||||
|
||||
public static SafeParcelableCreatorAndWriter<SimCapability> CREATOR =
|
||||
findCreator(SimCapability.class);
|
||||
@Field(1)
|
||||
public final int slotValue;
|
||||
@Field(2)
|
||||
public final String subscriberIdDigest;
|
||||
@Field(3)
|
||||
public final int carrierId;
|
||||
@Field(4)
|
||||
public final String operatorName;
|
||||
@Field(5)
|
||||
public final List<VerificationCapability> verificationCapabilities;
|
||||
|
||||
@Constructor
|
||||
public SimCapability(
|
||||
@Param(1) int slotValue,
|
||||
@Param(2) String subscriberIdDigest,
|
||||
@Param(3) int carrierId,
|
||||
@Param(4) String operatorName,
|
||||
@Param(5) List<VerificationCapability> verificationCapabilities
|
||||
) {
|
||||
this.slotValue = slotValue;
|
||||
this.subscriberIdDigest = subscriberIdDigest;
|
||||
this.carrierId = carrierId;
|
||||
this.operatorName = operatorName;
|
||||
this.verificationCapabilities = verificationCapabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@SafeParcelable.Class
|
||||
public static class VerificationCapability extends AbstractSafeParcelable {
|
||||
|
||||
public static SafeParcelableCreatorAndWriter<VerificationCapability> CREATOR =
|
||||
findCreator(VerificationCapability.class);
|
||||
@Field(1)
|
||||
public final int verificationMethod;
|
||||
@Field(2)
|
||||
public final int statusValue;
|
||||
|
||||
@Constructor
|
||||
public VerificationCapability(
|
||||
@Param(1) int verificationMethod,
|
||||
@Param(2) int statusValue
|
||||
) {
|
||||
this.verificationMethod = verificationMethod;
|
||||
this.statusValue = statusValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class PhoneNumberInfo extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<PhoneNumberInfo> CREATOR =
|
||||
findCreator(PhoneNumberInfo.class);
|
||||
|
||||
@Field(1)
|
||||
public final int version;
|
||||
|
||||
@Field(2)
|
||||
@Nullable
|
||||
public final String phoneNumber;
|
||||
|
||||
@Field(3)
|
||||
public final long verificationTime;
|
||||
|
||||
@Field(4)
|
||||
@Nullable
|
||||
public final Bundle extras;
|
||||
|
||||
@Constructor
|
||||
public PhoneNumberInfo(
|
||||
@SafeParcelable.Param(1) int version,
|
||||
@SafeParcelable.Param(2) @Nullable String phoneNumber,
|
||||
@SafeParcelable.Param(3) long verificationTime,
|
||||
@SafeParcelable.Param(4) @Nullable Bundle extras
|
||||
) {
|
||||
this.version = version;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.verificationTime = verificationTime;
|
||||
this.extras = extras;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class VerifyPhoneNumberRequest extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<VerifyPhoneNumberRequest> CREATOR =
|
||||
findCreator(VerifyPhoneNumberRequest.class);
|
||||
|
||||
@Field(1)
|
||||
public final String policyId;
|
||||
|
||||
@Field(2)
|
||||
public final long timeout;
|
||||
|
||||
@Field(3)
|
||||
public final IdTokenRequest idTokenRequest;
|
||||
|
||||
@Field(4)
|
||||
public final Bundle extras;
|
||||
|
||||
@Field(5)
|
||||
public final List<ImsiRequest> targetedSims;
|
||||
|
||||
@Field(6)
|
||||
public final boolean includeUnverified;
|
||||
|
||||
@Field(7)
|
||||
public final int apiVersion;
|
||||
|
||||
@Field(8)
|
||||
public final List<Integer> verificationMethodsValues;
|
||||
|
||||
@Constructor
|
||||
public VerifyPhoneNumberRequest(
|
||||
@Param(1) String policyId,
|
||||
@Param(2) long timeout,
|
||||
@Param(3) IdTokenRequest idTokenRequest,
|
||||
@Param(4) Bundle extras,
|
||||
@Param(5) List<ImsiRequest> targetedSims,
|
||||
@Param(6) boolean includeUnverified,
|
||||
@Param(7) int apiVersion,
|
||||
@Param(8) List<Integer> verificationMethodsValues
|
||||
) {
|
||||
this.policyId = policyId;
|
||||
this.timeout = timeout;
|
||||
this.idTokenRequest = idTokenRequest;
|
||||
this.extras = extras;
|
||||
this.targetedSims = targetedSims;
|
||||
this.includeUnverified = includeUnverified;
|
||||
this.apiVersion = apiVersion;
|
||||
this.verificationMethodsValues = verificationMethodsValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
|
||||
@SafeParcelable.Class
|
||||
public static class IdTokenRequest extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<IdTokenRequest> CREATOR =
|
||||
findCreator(IdTokenRequest.class);
|
||||
@Field(1)
|
||||
public final String idToken;
|
||||
@Field(2)
|
||||
public final String subscriberHash;
|
||||
|
||||
@Constructor
|
||||
public IdTokenRequest(
|
||||
@Param(1) String idToken,
|
||||
@Param(2) String subscriberHash
|
||||
) {
|
||||
this.idToken = idToken;
|
||||
this.subscriberHash = subscriberHash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@SafeParcelable.Class
|
||||
public static class ImsiRequest extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<ImsiRequest> CREATOR =
|
||||
findCreator(ImsiRequest.class);
|
||||
@Field(1)
|
||||
public final String imsi;
|
||||
@Field(2)
|
||||
public final String phoneNumberHint;
|
||||
|
||||
@Constructor
|
||||
public ImsiRequest(
|
||||
@Param(1) String imsi,
|
||||
@Param(2) String phoneNumberHint
|
||||
) {
|
||||
this.imsi = imsi;
|
||||
this.phoneNumberHint = phoneNumberHint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.google.android.gms.constellation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
|
||||
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
|
||||
|
||||
@SafeParcelable.Class
|
||||
public class VerifyPhoneNumberResponse extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<VerifyPhoneNumberResponse> CREATOR =
|
||||
findCreator(VerifyPhoneNumberResponse.class);
|
||||
|
||||
@Field(1)
|
||||
public final PhoneNumberVerification[] verifications;
|
||||
|
||||
@Field(2)
|
||||
public final Bundle extras;
|
||||
|
||||
@Constructor
|
||||
public VerifyPhoneNumberResponse(
|
||||
@Param(1) PhoneNumberVerification[] verifications,
|
||||
@Param(2) Bundle extras
|
||||
) {
|
||||
this.verifications = verifications;
|
||||
this.extras = extras;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
|
||||
@SafeParcelable.Class
|
||||
public static class PhoneNumberVerification extends AbstractSafeParcelable {
|
||||
public static SafeParcelableCreatorAndWriter<PhoneNumberVerification> CREATOR =
|
||||
findCreator(PhoneNumberVerification.class);
|
||||
@Field(1)
|
||||
@Nullable
|
||||
public final String phoneNumber;
|
||||
@Field(2)
|
||||
public final long timestampMillis;
|
||||
@Field(3)
|
||||
public final int verificationMethod;
|
||||
@Field(4)
|
||||
public final int simSlot;
|
||||
@Field(5)
|
||||
@Nullable
|
||||
public final String verificationToken;
|
||||
@Field(6)
|
||||
@Nullable
|
||||
public final Bundle extras;
|
||||
@Field(7)
|
||||
public final int verificationStatus;
|
||||
@Field(8)
|
||||
public final long retryAfterSeconds;
|
||||
|
||||
@Constructor
|
||||
public PhoneNumberVerification(
|
||||
@Param(1) @Nullable String phoneNumber,
|
||||
@Param(2) long timestampMillis,
|
||||
@Param(3) int verificationMethod,
|
||||
@Param(4) int simSlot,
|
||||
@Param(5) @Nullable String verificationToken,
|
||||
@Param(6) @Nullable Bundle extras,
|
||||
@Param(7) int verificationStatus,
|
||||
@Param(8) long retryAfterSeconds
|
||||
) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.timestampMillis = timestampMillis;
|
||||
this.verificationMethod = verificationMethod;
|
||||
this.simSlot = simSlot;
|
||||
this.verificationToken = verificationToken;
|
||||
this.extras = extras;
|
||||
this.verificationStatus = verificationStatus;
|
||||
this.retryAfterSeconds = retryAfterSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
CREATOR.writeToParcel(this, dest, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,11 +32,13 @@ include ':play-services-auth-api-phone'
|
|||
include ':play-services-auth-base'
|
||||
include ':play-services-auth-blockstore'
|
||||
include ':play-services-auth-workaccount'
|
||||
include ':play-services-asterism'
|
||||
include ':play-services-base'
|
||||
include ':play-services-basement'
|
||||
include ':play-services-cast'
|
||||
include ':play-services-cast-framework'
|
||||
include ':play-services-clearcut'
|
||||
include ':play-services-constellation'
|
||||
include ':play-services-drive'
|
||||
include ':play-services-droidguard'
|
||||
include ':play-services-fido'
|
||||
|
|
@ -88,12 +90,14 @@ sublude ':play-services-appset:core'
|
|||
sublude ':play-services-auth-api-phone:core'
|
||||
sublude ':play-services-auth-blockstore:core'
|
||||
sublude ':play-services-auth-workaccount:core'
|
||||
sublude ':play-services-asterism:core'
|
||||
sublude ':play-services-base:core'
|
||||
sublude ':play-services-base:core:package'
|
||||
sublude ':play-services-cast:core'
|
||||
sublude ':play-services-cast-framework:core'
|
||||
include ':play-services-chimera-core'
|
||||
include ':play-services-conscrypt-provider-core'
|
||||
sublude ':play-services-constellation:core'
|
||||
sublude ':play-services-cronet:core'
|
||||
sublude ':play-services-droidguard:core'
|
||||
sublude ':play-services-fido:core'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue