This commit is contained in:
water 2026-07-24 06:56:33 +08:00 committed by GitHub
commit 5d816bc80e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 7041 additions and 0 deletions

View 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")
}

View 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')
}

View 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>

View file

@ -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
)
}

View file

@ -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
}

View file

@ -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) }
}
}

View file

@ -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)
}
}

View file

@ -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, "", "")
)
}
}

View file

@ -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
)
}

View 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 />

View file

@ -0,0 +1,3 @@
package com.google.android.gms.asterism;
parcelable GetAsterismConsentRequest;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.asterism;
parcelable GetAsterismConsentResponse;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.asterism;
parcelable SetAsterismConsentRequest;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.asterism;
parcelable SetAsterismConsentResponse;

View file

@ -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
);
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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")
}

View 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"
}

View file

@ -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>

View file

@ -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)

View file

@ -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) }

View file

@ -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
}

View file

@ -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
)
}
}
}

View file

@ -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"
}
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
)
}
}

View file

@ -0,0 +1,5 @@
package org.microg.gms.constellation.core
object GetPnvCapabilitiesApiPhenotype {
val FPNV_ALLOWED_CARRIER_IDS = ArrayList<Int>()
}

View file

@ -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
}

View file

@ -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"
}

View file

@ -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>()
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
)
}

View file

@ -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
}
}
}

View file

@ -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)
)
}

View file

@ -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)
)
}

View file

@ -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
}

View file

@ -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
)
)
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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) {
}
}
}

View file

@ -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))
}

View file

@ -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("&", "&amp;")
.replace("&amp;amp;", "&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()
}

View file

@ -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
}
}

View file

@ -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)
)
}
}

View file

@ -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()
}
}

File diff suppressed because it is too large Load diff

View 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 />

View file

@ -0,0 +1,3 @@
package com.google.android.gms.constellation;
parcelable GetIidTokenRequest;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.constellation;
parcelable GetIidTokenResponse;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.constellation;
parcelable GetPnvCapabilitiesRequest;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.constellation;
parcelable GetPnvCapabilitiesResponse;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.constellation;
parcelable PhoneNumberInfo;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.constellation;
parcelable VerifyPhoneNumberRequest;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.constellation;
parcelable VerifyPhoneNumberResponse;

View file

@ -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
);
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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'