From 5991207cfe744103d6e7c14b40c62ca8184e615a Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:26:55 -0400 Subject: [PATCH 01/63] Implement `play-services-constellation` --- play-services-constellation/build.gradle | 73 ++ .../src/main/AndroidManifest.xml | 17 + .../gms/constellation/GetIidTokenRequest.aidl | 3 + .../constellation/GetIidTokenResponse.aidl | 3 + .../GetPnvCapabilitiesRequest.aidl | 3 + .../GetPnvCapabilitiesResponse.aidl | 3 + .../gms/constellation/PhoneNumberInfo.aidl | 3 + .../VerifyPhoneNumberRequest.aidl | 3 + .../VerifyPhoneNumberResponse.aidl | 3 + .../internal/IConstellationApiService.aidl | 39 + .../internal/IConstellationCallbacks.aidl | 15 + .../android/gms/constellation/GetIidToken.kt | 43 + .../gms/constellation/GetPnvCapabilities.kt | 87 ++ .../gms/constellation/PhoneNumberInfo.kt | 28 + .../gms/constellation/VerifyPhoneNumber.kt | 95 ++ .../microg/gms/constellation/AuthManager.kt | 102 ++ .../constellation/ConstellationApiService.kt | 124 ++ .../constellation/ConstellationStateStore.kt | 180 +++ .../org/microg/gms/constellation/GServices.kt | 33 + .../microg/gms/constellation/GetIidToken.kt | 35 + .../gms/constellation/GetPnvCapabilities.kt | 120 ++ .../GetPnvCapabilitiesApiPhenotype.kt | 5 + .../constellation/GetVerifiedPhoneNumbers.kt | 137 +++ .../gms/constellation/IidTokenPhenotypes.kt | 9 + .../org/microg/gms/constellation/RpcClient.kt | 47 + .../gms/constellation/VerificationMappings.kt | 134 +++ .../VerificationSettingsPhenotypes.kt | 6 + .../gms/constellation/VerifyPhoneNumber.kt | 346 ++++++ .../VerifyPhoneNumberApiPhenotypes.kt | 37 + .../proto/builders/ClientInfoBuilder.kt | 173 +++ .../proto/builders/CommonBuilders.kt | 70 ++ .../proto/builders/GaiaInfoBuilder.kt | 74 ++ .../proto/builders/RequestBuildContext.kt | 20 + .../proto/builders/SyncRequestBuilder.kt | 190 +++ .../proto/builders/TelephonyInfoBuilder.kt | 143 +++ .../verification/CarrierIdVerifier.kt | 84 ++ .../verification/ChallengeProcessor.kt | 152 +++ .../verification/MoSmsVerifier.kt | 210 ++++ .../verification/MtSmsVerifier.kt | 95 ++ .../verification/RegisteredSmsVerifier.kt | 159 +++ .../verification/Ts43Verifier.kt | 431 +++++++ .../verification/ts43/EapAkaService.kt | 211 ++++ .../verification/ts43/Fips186Prf.kt | 108 ++ .../ts43/ServiceEntitlementExtension.kt | 200 ++++ .../src/main/proto/constellation.proto | 1025 +++++++++++++++++ play-services-core/build.gradle | 1 + .../src/main/AndroidManifest.xml | 11 +- .../google/android/gms/iid/InstanceID.java | 20 +- settings.gradle | 1 + 49 files changed, 5108 insertions(+), 3 deletions(-) create mode 100644 play-services-constellation/build.gradle create mode 100644 play-services-constellation/src/main/AndroidManifest.xml create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenRequest.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenResponse.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/PhoneNumberInfo.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberRequest.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberResponse.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationApiService.aidl create mode 100644 play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationCallbacks.aidl create mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt create mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt create mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt create mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/AuthManager.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationApiService.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationStateStore.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GServices.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetIidToken.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilities.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilitiesApiPhenotype.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetVerifiedPhoneNumbers.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/IidTokenPhenotypes.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/RpcClient.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationMappings.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationSettingsPhenotypes.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumber.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumberApiPhenotypes.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/ClientInfoBuilder.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/CommonBuilders.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/GaiaInfoBuilder.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/RequestBuildContext.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/SyncRequestBuilder.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/TelephonyInfoBuilder.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/CarrierIdVerifier.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ChallengeProcessor.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MoSmsVerifier.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MtSmsVerifier.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/RegisteredSmsVerifier.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/Ts43Verifier.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/EapAkaService.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/Fips186Prf.kt create mode 100644 play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/ServiceEntitlementExtension.kt create mode 100644 play-services-constellation/src/main/proto/constellation.proto diff --git a/play-services-constellation/build.gradle b/play-services-constellation/build.gradle new file mode 100644 index 0000000000..2301c04063 --- /dev/null +++ b/play-services-constellation/build.gradle @@ -0,0 +1,73 @@ +/* + * 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: 'kotlin-parcelize' +apply plugin: 'kotlin-kapt' +apply plugin: 'signing' + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.squareup.wire:wire-gradle-plugin:4.9.3' + } +} +apply plugin: 'com.squareup.wire' + +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 + } + + 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 { + 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') + + api 'com.squareup.wire:wire-runtime:4.9.3' + api 'com.squareup.wire:wire-grpc-client:4.9.3' + api 'com.squareup.okhttp3:okhttp:4.12.0' + + kapt project(":safe-parcel-processor") +} diff --git a/play-services-constellation/src/main/AndroidManifest.xml b/play-services-constellation/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bf92ddcaf3 --- /dev/null +++ b/play-services-constellation/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenRequest.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenRequest.aidl new file mode 100644 index 0000000000..4621cec67a --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.constellation; + +parcelable GetIidTokenRequest; diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenResponse.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenResponse.aidl new file mode 100644 index 0000000000..832407aa6e --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetIidTokenResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.constellation; + +parcelable GetIidTokenResponse; diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.aidl new file mode 100644 index 0000000000..ffd7ecad31 --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.constellation; + +parcelable GetPnvCapabilitiesRequest; \ No newline at end of file diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.aidl new file mode 100644 index 0000000000..1b318c7ec7 --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.constellation; + +parcelable GetPnvCapabilitiesResponse; \ No newline at end of file diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/PhoneNumberInfo.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/PhoneNumberInfo.aidl new file mode 100644 index 0000000000..b6889c7bd4 --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/PhoneNumberInfo.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.constellation; + +parcelable PhoneNumberInfo; diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberRequest.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberRequest.aidl new file mode 100644 index 0000000000..f5ca759758 --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.constellation; + +parcelable VerifyPhoneNumberRequest; diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberResponse.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberResponse.aidl new file mode 100644 index 0000000000..31eb20049c --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/VerifyPhoneNumberResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.constellation; + +parcelable VerifyPhoneNumberResponse; diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationApiService.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationApiService.aidl new file mode 100644 index 0000000000..912acc7371 --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationApiService.aidl @@ -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 + ); +} diff --git a/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationCallbacks.aidl b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationCallbacks.aidl new file mode 100644 index 0000000000..74ce532b86 --- /dev/null +++ b/play-services-constellation/src/main/aidl/com/google/android/gms/constellation/internal/IConstellationCallbacks.aidl @@ -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 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); +} diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt new file mode 100644 index 0000000000..53760184ab --- /dev/null +++ b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt @@ -0,0 +1,43 @@ +package com.google.android.gms.constellation + +import android.os.Parcel +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.SafeParcelable.Constructor +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter + +@SafeParcelable.Class +data class GetIidTokenRequest @Constructor constructor( + @JvmField @Param(1) @Field(1) val projectNumber: Long? +) : AbstractSafeParcelable() { + override fun writeToParcel(out: Parcel, flags: Int) { + CREATOR.writeToParcel(this, out, flags) + } + + companion object { + @JvmField + val CREATOR: SafeParcelableCreatorAndWriter = + findCreator(GetIidTokenRequest::class.java) + } +} + +@SafeParcelable.Class +data class GetIidTokenResponse @Constructor constructor( + @JvmField @Param(1) @Field(1) val iidToken: String, + @JvmField @Param(2) @Field(2) val fid: String, + @JvmField @Param(3) @Field(value = 3, type = "byte[]") val signature: ByteArray?, + @JvmField @Param(4) @Field(4) val timestamp: Long +) : AbstractSafeParcelable() { + + override fun writeToParcel(out: Parcel, flags: Int) { + CREATOR.writeToParcel(this, out, flags) + } + + companion object { + @JvmField + val CREATOR: SafeParcelableCreatorAndWriter = + findCreator(GetIidTokenResponse::class.java) + } +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt new file mode 100644 index 0000000000..4598d98bb1 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt @@ -0,0 +1,87 @@ +package com.google.android.gms.constellation + +import android.os.Parcel +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.SafeParcelable.Constructor +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param + +@SafeParcelable.Class +data class GetPnvCapabilitiesRequest @Constructor constructor( + @JvmField @Param(1) @Field(1) val policyId: String, + @JvmField @Param(2) @Field(2) val verificationTypes: List, + @JvmField @Param(3) @Field(3) val simSlotIndices: List +) : AbstractSafeParcelable() { + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(GetPnvCapabilitiesRequest::class.java) + } +} + +@SafeParcelable.Class +data class GetPnvCapabilitiesResponse @Constructor constructor( + @JvmField @Param(1) @Field(1) val simCapabilities: List +) : AbstractSafeParcelable() { + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(GetPnvCapabilitiesResponse::class.java) + } +} + +@SafeParcelable.Class +data class SimCapability @Constructor constructor( + @JvmField @Param(1) @Field(1) val slotValue: Int, + @JvmField @Param(2) @Field(2) val subscriberIdDigest: String, + @JvmField @Param(3) @Field(3) val carrierId: Int, + @JvmField @Param(4) @Field(4) val operatorName: String, + @JvmField @Param(5) @Field(5) val verificationCapabilities: List +) : AbstractSafeParcelable() { + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(SimCapability::class.java) + } +} + +@SafeParcelable.Class +data class VerificationCapability @Constructor constructor( + @JvmField @Param(1) @Field(1) val verificationMethod: Int, + @JvmField @Param(2) @Field(2) val statusValue: Int // Enums are typically passed as Ints in SafeParcelable +) : AbstractSafeParcelable() { + constructor(verificationMethod: Int, status: VerificationStatus) : this( + verificationMethod, + status.value + ) + + val status: VerificationStatus + get() = VerificationStatus.fromInt(statusValue) + + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(VerificationCapability::class.java) + } +} + +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 + } + } +} diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt new file mode 100644 index 0000000000..459a05b3cd --- /dev/null +++ b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt @@ -0,0 +1,28 @@ +package com.google.android.gms.constellation + +import android.os.Bundle +import android.os.Parcel +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.SafeParcelable.Constructor +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter + +@SafeParcelable.Class +data class PhoneNumberInfo @Constructor constructor( + @JvmField @Param(1) @Field(1) val version: Int, + @JvmField @Param(2) @Field(2) val phoneNumber: String?, + @JvmField @Param(3) @Field(3) val verificationTime: Long, + @JvmField @Param(4) @Field(4) val extras: Bundle? +) : AbstractSafeParcelable() { + + override fun writeToParcel(out: Parcel, flags: Int) { + CREATOR.writeToParcel(this, out, flags) + } + + companion object { + @JvmField + val CREATOR = findCreator(PhoneNumberInfo::class.java) + } +} diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt new file mode 100644 index 0000000000..e2f159a44c --- /dev/null +++ b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt @@ -0,0 +1,95 @@ +package com.google.android.gms.constellation + +import android.os.Bundle +import android.os.Parcel +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.SafeParcelable.Constructor +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter +import org.microg.gms.constellation.proto.VerificationMethod + +@SafeParcelable.Class +data class VerifyPhoneNumberRequest @Constructor constructor( + @JvmField @Param(1) @Field(1) val policyId: String, + @JvmField @Param(2) @Field(2) val timeout: Long, + @JvmField @Param(3) @Field(3) val idTokenRequest: IdTokenRequest, + @JvmField @Param(4) @Field(4) val extras: Bundle, + @JvmField @Param(5) @Field(5) val targetedSims: List, + @JvmField @Param(6) @Field(6) val silent: Boolean, + @JvmField @Param(7) @Field(7) val apiVersion: Int, + @JvmField @Param(8) @Field(8) val verificationTypes: List +) : AbstractSafeParcelable() { + val verificationMethods: List + get() = verificationTypes.mapNotNull { VerificationMethod.fromValue(it) } + + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(VerifyPhoneNumberRequest::class.java) + } +} + +@SafeParcelable.Class +data class IdTokenRequest @Constructor constructor( + @JvmField @Param(1) @Field(1) val idToken: String, + @JvmField @Param(2) @Field(2) val subscriberHash: String +) : AbstractSafeParcelable() { + + override fun writeToParcel(out: Parcel, flags: Int) { + CREATOR.writeToParcel(this, out, flags) + } + + companion object { + @JvmField + val CREATOR: SafeParcelableCreatorAndWriter = + findCreator(IdTokenRequest::class.java) + } +} + +@SafeParcelable.Class +data class ImsiRequest @Constructor constructor( + @JvmField @Param(1) @Field(1) val imsi: String, + @JvmField @Param(2) @Field(2) val phoneNumberHint: String +) : AbstractSafeParcelable() { + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(ImsiRequest::class.java) + } +} + +@SafeParcelable.Class +data class VerifyPhoneNumberResponse @Constructor constructor( + @JvmField @Param(1) @Field(1) val verifications: Array, + @JvmField @Param(2) @Field(2) val extras: Bundle +) : AbstractSafeParcelable() { + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(VerifyPhoneNumberResponse::class.java) + } +} + +@SafeParcelable.Class +data class PhoneNumberVerification @Constructor constructor( + @JvmField @Param(1) @Field(1) val phoneNumber: String?, + @JvmField @Param(2) @Field(2) val timestampMillis: Long, + @JvmField @Param(3) @Field(3) val verificationMethod: Int, + @JvmField @Param(4) @Field(4) val simSlot: Int, + @JvmField @Param(5) @Field(5) val verificationToken: String?, + @JvmField @Param(6) @Field(6) val extras: Bundle?, + @JvmField @Param(7) @Field(7) val verificationStatus: Int, + @JvmField @Param(8) @Field(8) val retryAfterSeconds: Long +) : AbstractSafeParcelable() { + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(PhoneNumberVerification::class.java) + } +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/AuthManager.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/AuthManager.kt new file mode 100644 index 0000000000..148d585ed3 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/AuthManager.kt @@ -0,0 +1,102 @@ +package org.microg.gms.constellation + +import android.content.Context +import android.util.Base64 +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 + +class AuthManager(context: Context) { + private val context = context.applicationContext + + companion object { + private const val PREFS_NAME = "constellation_prefs" + private const val KEY_PRIVATE = "private_key" + private const val KEY_PUBLIC = "public_key" + + @Volatile + private var instance: AuthManager? = null + + fun get(context: Context): AuthManager { + val existing = instance + if (existing != null) return existing + + return synchronized(this) { + instance ?: AuthManager(context).also { instance = it } + } + } + } + + private val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + // GMS signing format: {iidToken}:{seconds}:{nanos} + fun signIidToken(iidToken: String): Pair { + val now = System.currentTimeMillis() + val timestamp = Instant.ofEpochMilli(now) + val content = "$iidToken:${timestamp.epochSecond}:${timestamp.nano}" + return sign(content) to timestamp + } + + fun getIidToken(projectNumber: String? = null): String { + return try { + val sender = projectNumber ?: IidTokenPhenotypes.DEFAULT_PROJECT_NUMBER.toString() + 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 { + return InstanceID.getInstance(context).id + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationApiService.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationApiService.kt new file mode 100644 index 0000000000..a6829f734b --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationApiService.kt @@ -0,0 +1,124 @@ +package org.microg.gms.constellation + +import android.content.Context +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 + serviceScope.launch { handleVerifyPhoneNumberV1(context, cb, bundle, packageName) } + } + + override fun verifyPhoneNumberSingleUse( + cb: IConstellationCallbacks?, + bundle: Bundle?, + apiMetadata: ApiMetadata? + ) { + Log.i(TAG, "verifyPhoneNumberSingleUse()") + if (cb == null || bundle == null) return + serviceScope.launch { handleVerifyPhoneNumberSingleUse(context, 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 + serviceScope.launch { handleVerifyPhoneNumberRequest(context, 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, cb, request) } + } + + override fun getPnvCapabilities( + cb: IConstellationCallbacks?, + request: GetPnvCapabilitiesRequest?, + apiMetadata: ApiMetadata?, + ) { + Log.i(TAG, "getPnvCapabilities(): $request") + if (cb == null || request == null) return + serviceScope.launch { handleGetPnvCapabilities(context, cb, request) } + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationStateStore.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationStateStore.kt new file mode 100644 index 0000000000..5c6f07bd08 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationStateStore.kt @@ -0,0 +1,180 @@ +package org.microg.gms.constellation + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Base64 +import androidx.core.content.edit +import com.squareup.wire.Instant +import okio.ByteString.Companion.toByteString +import org.microg.gms.constellation.proto.DroidguardToken +import org.microg.gms.constellation.proto.ProceedResponse +import org.microg.gms.constellation.proto.Consent +import org.microg.gms.constellation.proto.ConsentSource +import org.microg.gms.constellation.proto.ConsentVersion +import org.microg.gms.constellation.proto.ServerTimestamp +import org.microg.gms.constellation.proto.SyncResponse +import org.microg.gms.constellation.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 { + 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 + } + + private fun storeCachedDroidGuardToken( + context: Context, + token: String, + expirationMillis: Long + ) { + if (token.isBlank() || expirationMillis <= System.currentTimeMillis()) return + statePrefs(context).edit { + putString(KEY_DROIDGUARD_TOKEN, token) + putLong(KEY_DROIDGUARD_TOKEN_TTL, expirationMillis) + } + } + + 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: ConsentVersion + ) { + 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) { + 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 + storeCachedDroidGuardToken(context, tokenValue, expiration) + } + + private fun storeNextSyncTime(context: Context, timestamp: ServerTimestamp?) { + val nextSyncDelayMillis = timestamp?.let(::nextSyncDelayMillis) ?: return + statePrefs(context).edit { + // GMS stores the next sync deadline as an absolute wall-clock timestamp + putLong(KEY_NEXT_SYNC_TIMESTAMP_MS, System.currentTimeMillis() + nextSyncDelayMillis) + } + } + + private fun nextSyncDelayMillis(timestamp: ServerTimestamp): Long { + val serverMillis = timestamp.timestamp?.toEpochMilli() ?: 0L + val localMillis = timestamp.now?.toEpochMilli() ?: 0L + return 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) +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GServices.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GServices.kt new file mode 100644 index 0000000000..ca5b6acbb9 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GServices.kt @@ -0,0 +1,33 @@ +package org.microg.gms.constellation + +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 + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetIidToken.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetIidToken.kt new file mode 100644 index 0000000000..08775f8b9f --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetIidToken.kt @@ -0,0 +1,35 @@ +package org.microg.gms.constellation + +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 = AuthManager.get(context) + val iidToken = authManager.getIidToken(request.projectNumber?.toString()) + val fid = authManager.getFid() + val (signature, timestamp) = authManager.signIidToken(iidToken) + + callbacks.onIidTokenGenerated( + Status.SUCCESS, + GetIidTokenResponse(iidToken, fid, signature, timestamp.toEpochMilli()), + ApiMetadata.DEFAULT + ) + } catch (e: Exception) { + Log.e(TAG, "getIidToken failed", e) + callbacks.onIidTokenGenerated(Status.INTERNAL_ERROR, null, ApiMetadata.DEFAULT) + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilities.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilities.kt new file mode 100644 index 0000000000..38fec9f3e8 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilities.kt @@ -0,0 +1,120 @@ +package org.microg.gms.constellation + +import android.Manifest +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.annotation.RequiresPermission +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.SimCapability +import com.google.android.gms.constellation.VerificationCapability +import com.google.android.gms.constellation.VerificationStatus +import com.google.android.gms.constellation.internal.IConstellationCallbacks +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.security.MessageDigest + +private const val TAG = "GetPnvCapabilities" + +@SuppressLint("HardwareIds") +@RequiresPermission( + allOf = + [ + Manifest.permission.READ_PHONE_STATE, + "android.permission.READ_PRIVILEGED_PHONE_STATE"] +) +@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) +suspend fun handleGetPnvCapabilities( + context: Context, + callbacks: IConstellationCallbacks, + request: GetPnvCapabilitiesRequest +) = withContext(Dispatchers.IO) { + try { + val baseTelephonyManager = + context.getSystemService() + ?: throw IllegalStateException("TelephonyManager unavailable") + val subscriptionManager = + context.getSystemService() + ?: 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 + ) + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilitiesApiPhenotype.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilitiesApiPhenotype.kt new file mode 100644 index 0000000000..814fae3f1c --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilitiesApiPhenotype.kt @@ -0,0 +1,5 @@ +package org.microg.gms.constellation + +object GetPnvCapabilitiesApiPhenotype { + val FPNV_ALLOWED_CARRIER_IDS = ArrayList() +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetVerifiedPhoneNumbers.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetVerifiedPhoneNumbers.kt new file mode 100644 index 0000000000..b382bef5a8 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetVerifiedPhoneNumbers.kt @@ -0,0 +1,137 @@ +package org.microg.gms.constellation + +import android.content.Context +import android.os.Bundle +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.PhoneNumberInfo +import com.google.android.gms.constellation.PhoneNumberVerification +import com.google.android.gms.constellation.VerifyPhoneNumberResponse +import com.google.android.gms.constellation.internal.IConstellationCallbacks +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString +import org.microg.gms.constellation.proto.GetVerifiedPhoneNumbersRequest +import org.microg.gms.constellation.proto.GetVerifiedPhoneNumbersRequest.PhoneNumberSelection +import org.microg.gms.constellation.proto.IIDTokenAuth +import org.microg.gms.constellation.proto.TokenOption +import org.microg.gms.constellation.proto.VerifiedPhoneNumber as RpcVerifiedPhoneNumber +import java.util.UUID + +private const val TAG = "GetVerifiedPhoneNumbers" + +suspend fun handleGetVerifiedPhoneNumbers( + context: Context, + callbacks: IConstellationCallbacks, + bundle: Bundle +) = withContext(Dispatchers.IO) { + try { + val phoneNumbers = fetchVerifiedPhoneNumbers(context, bundle).map { it.toPhoneNumberInfo() } + + callbacks.onPhoneNumberVerified(Status.SUCCESS, phoneNumbers, ApiMetadata.DEFAULT) + } catch (e: Exception) { + Log.e(TAG, "Error in GetVerifiedPhoneNumbers (read-only)", e) + callbacks.onPhoneNumberVerified(Status.INTERNAL_ERROR, emptyList(), ApiMetadata.DEFAULT) + } +} + +internal suspend fun fetchVerifiedPhoneNumbers( + context: Context, + bundle: Bundle, + callingPackage: String = bundle.getString("calling_package") ?: context.packageName +): List = withContext(Dispatchers.IO) { + val authManager = AuthManager.get(context) + 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() + 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 List.toVerifyPhoneNumberResponse(): VerifyPhoneNumberResponse { + return VerifyPhoneNumberResponse(map { it.toPhoneNumberVerification() }.toTypedArray(), Bundle.EMPTY) +} + +private fun RpcVerifiedPhoneNumber.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 + ) +} + +private fun RpcVerifiedPhoneNumber.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 { + val selections = mutableListOf() + 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 +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/IidTokenPhenotypes.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/IidTokenPhenotypes.kt new file mode 100644 index 0000000000..a700f12f2f --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/IidTokenPhenotypes.kt @@ -0,0 +1,9 @@ +package org.microg.gms.constellation + +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 +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/RpcClient.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/RpcClient.kt new file mode 100644 index 0000000000..f7a49733aa --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/RpcClient.kt @@ -0,0 +1,47 @@ +package org.microg.gms.constellation + +import com.squareup.wire.GrpcClient +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import org.microg.gms.common.Constants +import org.microg.gms.constellation.proto.PhoneDeviceVerificationClient +import org.microg.gms.constellation.proto.PhoneNumberClient + +private class AuthInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + 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()) + + return chain.proceed(builder.build()) + } +} + +object RpcClient { + private val client: OkHttpClient by lazy { + OkHttpClient.Builder() + .addInterceptor(AuthInterceptor()) + .build() + } + + private val grpcClient: GrpcClient by lazy { + 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 by lazy { + grpcClient.create(PhoneDeviceVerificationClient::class) + } + + val phoneNumberClient: PhoneNumberClient by lazy { + grpcClient.create(PhoneNumberClient::class) + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationMappings.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationMappings.kt new file mode 100644 index 0000000000..55bbdda27e --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationMappings.kt @@ -0,0 +1,134 @@ +package org.microg.gms.constellation + +import android.os.Bundle +import com.google.android.gms.constellation.PhoneNumberVerification +import org.microg.gms.constellation.proto.Param +import org.microg.gms.constellation.proto.UnverifiedInfo +import org.microg.gms.constellation.proto.Verification +import org.microg.gms.constellation.proto.VerificationMethod + +fun UnverifiedInfo.Reason.toVerificationStatus(): Verification.Status { + return when (this) { + UnverifiedInfo.Reason.UNKNOWN_REASON -> Verification.Status.STATUS_PENDING + 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 + } +} + +fun Verification.getState(): Verification.State { + return when { + verification_info != null -> Verification.State.VERIFIED + pending_verification_info != null -> Verification.State.PENDING + unverified_info != null -> Verification.State.NONE + else -> Verification.State.UNKNOWN + } +} + +fun Verification.getVerificationStatus(): Verification.Status { + return when (getState()) { + 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_PENDING + } + + Verification.State.UNKNOWN -> Verification.Status.STATUS_UNKNOWN + } +} + +fun Verification.toClientVerification(imsiToSlotMap: Map): PhoneNumberVerification { + val verificationStatus = this.getVerificationStatus() + var phoneNumber: String? = null + var timestampMillis = System.currentTimeMillis() + var verificationMethod = VerificationMethod.UNKNOWN + var retryAfterSeconds = 0L + val extras = buildClientExtras() + + when (this.getState()) { + Verification.State.VERIFIED -> { + val info = this.verification_info + phoneNumber = info?.phone_number + timestampMillis = info?.verification_time?.toEpochMilli() ?: System.currentTimeMillis() + verificationMethod = info?.challenge_method ?: VerificationMethod.UNKNOWN + } + + Verification.State.PENDING -> { + verificationMethod = + this.pending_verification_info?.challenge?.type ?: VerificationMethod.UNKNOWN + } + + Verification.State.NONE -> { + val info = this.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) + } ?: 0L + } + + else -> {} + } + + val simImsi = this.association?.sim?.sim_info?.imsi?.firstOrNull() + val simSlot = if (simImsi != null) imsiToSlotMap[simImsi] ?: 0 else 0 + val verificationToken = extras.getString("id_token") + + return PhoneNumberVerification( + phoneNumber, + timestampMillis, + verificationMethod.toClientMethod(), + simSlot, + verificationToken, + extras, + verificationStatus.value, + 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) { + // GMS exposes this as a string inside the extras bundle. + 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.TS43 -> 9 + VerificationMethod.UNKNOWN -> 0 + else -> value + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationSettingsPhenotypes.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationSettingsPhenotypes.kt new file mode 100644 index 0000000000..c51f0a279e --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationSettingsPhenotypes.kt @@ -0,0 +1,6 @@ +package org.microg.gms.constellation + +object VerificationSettingsPhenotypes { + const val A2P_SMS_SIGNAL_GRANULARITY_HRS = 1L + const val A2P_HISTORY_WINDOW_HOURS = 168L +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumber.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumber.kt new file mode 100644 index 0000000000..d0dc3b314c --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumber.kt @@ -0,0 +1,346 @@ +package org.microg.gms.constellation + +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.IdTokenRequest +import com.google.android.gms.constellation.PhoneNumberInfo +import com.google.android.gms.constellation.PhoneNumberVerification +import com.google.android.gms.constellation.VerifyPhoneNumberRequest +import com.google.android.gms.constellation.VerifyPhoneNumberResponse +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.constellation.proto.SyncRequest +import org.microg.gms.constellation.proto.Verification +import org.microg.gms.constellation.proto.builders.RequestBuildContext +import org.microg.gms.constellation.proto.builders.buildImsiToSubscriptionInfoMap +import org.microg.gms.constellation.proto.builders.buildRequestContext +import org.microg.gms.constellation.proto.builders.invoke +import org.microg.gms.constellation.verification.ChallengeProcessor +import java.util.UUID + +private const val TAG = "VerifyPhoneNumber" + +private enum class ReadCallbackMode { + NONE, + LEGACY, + TYPED +} + +suspend fun handleVerifyPhoneNumberV1( + context: Context, + callbacks: IConstellationCallbacks, + bundle: Bundle, + packageName: String? +) { + val callingPackage = packageName ?: bundle.getString("calling_package") ?: context.packageName + 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( + policyId = extras.getString("policy_id", ""), + timeout = timeout, + idTokenRequest = IdTokenRequest( + extras.getString("certificate_hash", ""), + extras.getString("token_nonce", "") + ), + extras = extras, + targetedSims = emptyList(), + silent = false, + apiVersion = 2, + verificationTypes = 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 + ) +} + +suspend fun handleVerifyPhoneNumberSingleUse( + context: Context, + callbacks: IConstellationCallbacks, + bundle: Bundle, + packageName: String? +) { + val callingPackage = packageName ?: bundle.getString("calling_package") ?: context.packageName + 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( + policyId = extras.getString("policy_id", ""), + timeout = timeout, + idTokenRequest = IdTokenRequest( + extras.getString("certificate_hash", ""), + extras.getString("token_nonce", "") + ), + extras = extras, + targetedSims = emptyList(), + silent = false, + apiVersion = 2, + verificationTypes = emptyList() + ) + + handleVerifyPhoneNumberRequest( + context, + callbacks, + request, + callingPackage, + ReadCallbackMode.NONE, + legacyCallbackOnFullFlow = true + ) +} + +suspend fun handleVerifyPhoneNumberRequest( + context: Context, + callbacks: IConstellationCallbacks, + request: VerifyPhoneNumberRequest, + packageName: String? +) { + val callingPackage = packageName ?: context.packageName + val requestWithExtras = request.copy( + extras = Bundle(request.extras).apply { + putString("calling_api", "verifyPhoneNumber") + } + ) + val useReadPath = when (requestWithExtras.apiVersion) { + 0 -> requestWithExtras.policyId in VerifyPhoneNumberApiPhenotypes.READ_ONLY_POLICY_IDS + 2 -> VerifyPhoneNumberApiPhenotypes.ENABLE_READ_FLOW + 3 -> requestWithExtras.policyId in VerifyPhoneNumberApiPhenotypes.POLICY_IDS_ALLOWED_FOR_LOCAL_READ + else -> false + } + + handleVerifyPhoneNumberRequest( + context, + callbacks, + requestWithExtras, + callingPackage, + if (useReadPath) ReadCallbackMode.TYPED else ReadCallbackMode.NONE, + localReadFallback = requestWithExtras.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 +) { + try { + when (readCallbackMode) { + ReadCallbackMode.LEGACY -> { + Log.d(TAG, "Using read-only mode") + handleGetVerifiedPhoneNumbers(context, callbacks, request.extras) + } + + 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") + } + val response = fetchVerifiedPhoneNumbers(context, request.extras, callingPackage) + .toVerifyPhoneNumberResponse() + callbacks.onPhoneNumberVerificationsCompleted( + Status.SUCCESS, + response, + ApiMetadata.DEFAULT + ) + } + + ReadCallbackMode.NONE -> { + Log.d(TAG, "Using full verification mode") + runVerificationFlow( + context, + request, + callingPackage, + callbacks, + legacyCallback = legacyCallbackOnFullFlow + ) + } + } + } catch (e: Exception) { + Log.e(TAG, "verifyPhoneNumber failed", e) + when (readCallbackMode) { + ReadCallbackMode.LEGACY -> { + callbacks.onPhoneNumberVerified(Status.INTERNAL_ERROR, emptyList(), ApiMetadata.DEFAULT) + } + + ReadCallbackMode.NONE -> { + if (legacyCallbackOnFullFlow) { + callbacks.onPhoneNumberVerified(Status.INTERNAL_ERROR, emptyList(), ApiMetadata.DEFAULT) + } else { + callbacks.onPhoneNumberVerificationsCompleted( + Status.INTERNAL_ERROR, + VerifyPhoneNumberResponse(emptyArray(), Bundle()), + ApiMetadata.DEFAULT + ) + } + } + + ReadCallbackMode.TYPED -> { + callbacks.onPhoneNumberVerificationsCompleted( + Status.INTERNAL_ERROR, + VerifyPhoneNumberResponse(emptyArray(), Bundle()), + ApiMetadata.DEFAULT + ) + } + } + } +} + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) +private suspend fun runVerificationFlow( + context: Context, + request: VerifyPhoneNumberRequest, + callingPackage: String, + callbacks: IConstellationCallbacks, + legacyCallback: Boolean +) { + val sessionId = UUID.randomUUID().toString() + + val imsiToInfoMap = buildImsiToSubscriptionInfoMap(context) + val buildContext = buildRequestContext(context) + val syncRequest = SyncRequest( + context, + sessionId, + request, + buildContext, + imsiToInfoMap = imsiToInfoMap, + includeClientAuth = ConstellationStateStore.isPublicKeyAcked(context), + callingPackage = callingPackage + ) + val (verifications, isPublicKeyAcked) = executeSyncFlow( + context, + sessionId, + request, + syncRequest, + buildContext, + imsiToInfoMap + ) + if (isPublicKeyAcked) { + ConstellationStateStore.setPublicKeyAcked(context, true) + } + + if (legacyCallback) { + callbacks.onPhoneNumberVerified( + Status.SUCCESS, + verifications.mapNotNull { it.toLegacyPhoneNumberInfoOrNull() }, + ApiMetadata.DEFAULT + ) + } else { + callbacks.onPhoneNumberVerificationsCompleted( + Status.SUCCESS, + VerifyPhoneNumberResponse(verifications, Bundle()), + ApiMetadata.DEFAULT + ) + } +} + +private fun PhoneNumberVerification.toLegacyPhoneNumberInfoOrNull(): PhoneNumberInfo? { + if (verificationStatus != Verification.Status.STATUS_VERIFIED.value || phoneNumber.isNullOrEmpty()) { + return null + } + val extras = Bundle(this.extras ?: Bundle.EMPTY).apply { + verificationToken?.let { putString("id_token", it) } + } + return PhoneNumberInfo(1, phoneNumber, timestampMillis, extras) +} + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) +private suspend fun executeSyncFlow( + context: Context, + sessionId: String, + request: VerifyPhoneNumberRequest, + syncRequest: SyncRequest, + buildContext: RequestBuildContext, + imsiToInfoMap: Map +): Pair, Boolean> = 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.getState() == Verification.State.PENDING) { + ChallengeProcessor.process( + context, + sessionId, + imsiToInfoMap, + buildContext, + verification + ) + } else { + verification + } + + finalVerification.toClientVerification(imsiToSlotMap) + }.toTypedArray() + + if (isPublicKeyAcked) { + Log.d(TAG, "Server acknowledged client public key") + } + + verifications to isPublicKeyAcked +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumberApiPhenotypes.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumberApiPhenotypes.kt new file mode 100644 index 0000000000..93faca2dd1 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumberApiPhenotypes.kt @@ -0,0 +1,37 @@ +package org.microg.gms.constellation + +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 +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/ClientInfoBuilder.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/ClientInfoBuilder.kt new file mode 100644 index 0000000000..99e224cb3d --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/ClientInfoBuilder.kt @@ -0,0 +1,173 @@ +package org.microg.gms.constellation.proto.builders + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.util.Log +import androidx.annotation.RequiresPermission +import androidx.core.content.edit +import androidx.core.content.getSystemService +import com.google.android.gms.tasks.await +import okio.ByteString.Companion.toByteString +import org.microg.gms.common.Constants +import org.microg.gms.constellation.AuthManager +import org.microg.gms.constellation.ConstellationStateStore +import org.microg.gms.constellation.GServices +import org.microg.gms.constellation.proto.ClientInfo +import org.microg.gms.constellation.proto.CountryInfo +import org.microg.gms.constellation.proto.DeviceID +import org.microg.gms.constellation.proto.DeviceType +import org.microg.gms.constellation.proto.DroidGuardSignals +import org.microg.gms.constellation.proto.GaiaSignals +import org.microg.gms.constellation.proto.GaiaToken +import org.microg.gms.constellation.proto.NetworkSignal +import org.microg.gms.constellation.proto.SimOperatorInfo +import org.microg.gms.constellation.proto.UserProfileType +import java.util.Locale + +private const val TAG = "ClientInfoBuilder" +private const val PREFS_NAME = "constellation_prefs" + +@RequiresPermission(Manifest.permission.GET_ACCOUNTS) +@SuppressLint("HardwareIds") +suspend operator fun ClientInfo.Companion.invoke(context: Context, iidToken: String): ClientInfo { + return ClientInfo( + context, + RequestBuildContext( + iidToken = iidToken, + gaiaTokens = GaiaToken.getList(context) + ) + ) +} + +@RequiresPermission(Manifest.permission.GET_ACCOUNTS) +@SuppressLint("HardwareIds") +suspend operator fun ClientInfo.Companion.invoke( + context: Context, + buildContext: RequestBuildContext +): ClientInfo { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + val locale = Locale.getDefault().let { "${it.language}_${it.country}" } + val authManager = AuthManager.get(context) + + return ClientInfo( + device_id = DeviceID(context, buildContext.iidToken), + client_public_key = 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), + ) +} + +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?.getSerialNumberForUser(android.os.Process.myUserHandle()) ?: 0L + } catch (_: Exception) { + 0L + } + + var primaryDeviceId = prefs.getLong("primary_device_id", 0L) + val isSystemUser = try { + val userManager = context.getSystemService() + 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() + val networkCountries = mutableListOf() + + try { + val sm = context.getSystemService() + val tm = context.getSystemService() + + sm?.activeSubscriptionInfoList?.forEach { info -> + val targetTM = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + tm?.createForSubscriptionId(info.subscriptionId) + } else tm + + 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() + val simCountry = tm?.simCountryIso?.lowercase()?.takeIf { it.isNotBlank() } + if (simCountry != null) simCountries.add(simCountry) + } + if (networkCountries.isEmpty()) { + val tm = context.getSystemService() + 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): DroidGuardSignals? { + val cachedToken = ConstellationStateStore.loadDroidGuardToken(context) + if (!cachedToken.isNullOrBlank()) { + return DroidGuardSignals(droidguard_token = cachedToken, droidguard_result = "") + } + + return try { + val client = com.google.android.gms.droidguard.DroidGuard.getClient(context) + val data = mapOf( + "package_name" to context.packageName, + "timestamp" to System.currentTimeMillis().toString() + ) + 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 + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/CommonBuilders.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/CommonBuilders.kt new file mode 100644 index 0000000000..b05e0dc40c --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/CommonBuilders.kt @@ -0,0 +1,70 @@ +package org.microg.gms.constellation.proto.builders + +import android.Manifest +import android.content.Context +import android.os.Bundle +import androidx.annotation.RequiresPermission +import okio.ByteString.Companion.toByteString +import org.microg.gms.constellation.AuthManager +import org.microg.gms.constellation.proto.AuditToken +import org.microg.gms.constellation.proto.AuditTokenMetadata +import org.microg.gms.constellation.proto.AuditUuid +import org.microg.gms.constellation.proto.ClientInfo +import org.microg.gms.constellation.proto.DeviceID +import org.microg.gms.constellation.proto.Param +import org.microg.gms.constellation.proto.RequestHeader +import org.microg.gms.constellation.proto.RequestTrigger + +fun Param.Companion.getList(extras: Bundle?): List { + if (extras == null) return emptyList() + val params = mutableListOf() + val ignoreKeys = setOf("consent_variant_key", "consent_trigger_key", "gaia_access_token") + for (key in extras.keySet()) { + if (key !in ignoreKeys) { + extras.get(key)?.toString()?.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 + ) + ) + ) +} + +@RequiresPermission(Manifest.permission.GET_ACCOUNTS) +suspend operator fun RequestHeader.Companion.invoke( + context: Context, + sessionId: String, + buildContext: RequestBuildContext, + triggerType: RequestTrigger.Type = RequestTrigger.Type.CONSENT_API_TRIGGER, + includeClientAuth: Boolean = false +): RequestHeader { + val authManager = if (includeClientAuth) AuthManager.get(context) else null + val clientAuth = if (includeClientAuth) { + val (signature, timestamp) = authManager!!.signIidToken(buildContext.iidToken) + org.microg.gms.constellation.proto.ClientAuth( + device_id = DeviceID(context, buildContext.iidToken), + signature = signature.toByteString(), + sign_timestamp = timestamp + ) + } else { + null + } + + return RequestHeader( + client_info = ClientInfo(context, buildContext), + client_auth = clientAuth, + session_id = sessionId, + trigger = RequestTrigger(type = triggerType) + ) +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/GaiaInfoBuilder.kt new file mode 100644 index 0000000000..dd2b34b42d --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/GaiaInfoBuilder.kt @@ -0,0 +1,74 @@ +package org.microg.gms.constellation.proto.builders + +import android.Manifest +import android.accounts.AccountManager +import android.content.Context +import android.util.Log +import androidx.annotation.RequiresPermission +import com.squareup.wire.Instant +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.constellation.proto.GaiaAccountSignalType +import org.microg.gms.constellation.proto.GaiaSignalEntry +import org.microg.gms.constellation.proto.GaiaSignals +import org.microg.gms.constellation.proto.GaiaToken +import java.security.MessageDigest +import kotlin.math.abs +import kotlin.math.absoluteValue + +private const val TAG = "GaiaInfoBuilder" + +@RequiresPermission(Manifest.permission.GET_ACCOUNTS) +operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? { + val entries = mutableListOf() + try { + val accounts = AccountManager.get(context).getAccountsByType("com.google") + val md = MessageDigest.getInstance("SHA-256") + + for (account in accounts) { + val hash = md.digest(account.name.toByteArray(Charsets.UTF_8)) + val number = + hash.take(8).fold(0L) { acc, byte -> (acc shl 8) or (byte.toLong() and 0xFF) } + val obfuscatedId = try { + number.absoluteValue.toString() + } catch (_: Exception) { + abs(account.name.hashCode().toLong()).toString() + } + + entries.add( + GaiaSignalEntry( + gaia_id = obfuscatedId, + signal_type = GaiaAccountSignalType.GAIA_ACCOUNT_SIGNAL_AUTHENTICATED, + timestamp = Instant.ofEpochMilli(System.currentTimeMillis()) + ) + ) + } + } catch (e: Exception) { + Log.w(TAG, "Could not build Gaia signals", e) + } + return if (entries.isNotEmpty()) GaiaSignals(gaia_signals = entries) else null +} + +@Suppress("DEPRECATION") +suspend fun GaiaToken.Companion.getList(context: Context): List = + withContext(Dispatchers.IO) { + val gaiaTokens = mutableListOf() + try { + val accounts = AccountManager.get(context).getAccountsByType("com.google") + if (accounts.isNotEmpty()) { + val future = AccountManager.get(context).getAuthToken( + accounts.first(), + "oauth2:https://www.googleapis.com/auth/numberer", + null, + false, + null, + null + ) + val token = future.result?.getString(AccountManager.KEY_AUTHTOKEN) + if (!token.isNullOrBlank()) gaiaTokens.add(GaiaToken(token = token)) + } + } catch (e: Exception) { + Log.w(TAG, "Could not retrieve Gaia tokens", e) + } + gaiaTokens + } diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/RequestBuildContext.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/RequestBuildContext.kt new file mode 100644 index 0000000000..5ac41bba97 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/RequestBuildContext.kt @@ -0,0 +1,20 @@ +package org.microg.gms.constellation.proto.builders + +import android.content.Context +import org.microg.gms.constellation.AuthManager +import org.microg.gms.constellation.proto.GaiaToken + +data class RequestBuildContext( + val iidToken: String, + val gaiaTokens: List +) + +suspend fun buildRequestContext( + context: Context, + authManager: AuthManager = AuthManager.get(context) +): RequestBuildContext { + return RequestBuildContext( + iidToken = authManager.getIidToken(), + gaiaTokens = GaiaToken.getList(context) + ) +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/SyncRequestBuilder.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/SyncRequestBuilder.kt new file mode 100644 index 0000000000..7d86e3964f --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/SyncRequestBuilder.kt @@ -0,0 +1,190 @@ +package org.microg.gms.constellation.proto.builders + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import com.google.android.gms.constellation.VerifyPhoneNumberRequest +import org.microg.gms.constellation.ConstellationStateStore +import org.microg.gms.constellation.proto.ChallengePreference +import org.microg.gms.constellation.proto.ChallengePreferenceMetadata +import org.microg.gms.constellation.proto.IdTokenRequest +import org.microg.gms.constellation.proto.Param +import org.microg.gms.constellation.proto.RequestHeader +import org.microg.gms.constellation.proto.RequestTrigger +import org.microg.gms.constellation.proto.SIMAssociation +import org.microg.gms.constellation.proto.SIMSlotInfo +import org.microg.gms.constellation.proto.SyncRequest +import org.microg.gms.constellation.proto.TelephonyInfo +import org.microg.gms.constellation.proto.TelephonyPhoneNumberType +import org.microg.gms.constellation.proto.Verification +import org.microg.gms.constellation.proto.VerificationAssociation +import org.microg.gms.constellation.proto.VerificationParam +import org.microg.gms.constellation.proto.VerificationPolicy + +private const val TAG = "SyncRequestBuilder" + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) +@SuppressLint("HardwareIds", "MissingPermission") +fun buildImsiToSubscriptionInfoMap(context: Context): Map { + val subscriptionManager = + context.getSystemService() ?: return emptyMap() + val telephonyManager = + context.getSystemService() ?: return emptyMap() + val map = mutableMapOf() + + try { + subscriptionManager.activeSubscriptionInfoList?.forEach { info -> + val subsTelephonyManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + telephonyManager.createForSubscriptionId(info.subscriptionId) + } else { + telephonyManager + } + 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 +} + +@SuppressLint("MissingPermission") +fun getTelephonyPhoneNumbers( + context: Context, + subscriptionId: Int +): List { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return emptyList() + + val subscriptionManager = + context.getSystemService() ?: 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() + } +} + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) +@SuppressLint("HardwareIds", "MissingPermission") +suspend operator fun SyncRequest.Companion.invoke( + context: Context, + sessionId: String, + request: VerifyPhoneNumberRequest, + includeClientAuth: Boolean = false, + callingPackage: String = context.packageName, + 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 + ) +} + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) +@SuppressLint("HardwareIds", "MissingPermission") +suspend operator fun SyncRequest.Companion.invoke( + context: Context, + sessionId: String, + request: VerifyPhoneNumberRequest, + buildContext: RequestBuildContext, + imsiToInfoMap: Map = buildImsiToSubscriptionInfoMap(context), + includeClientAuth: Boolean = false, + callingPackage: String = context.packageName, + 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 = subscriptionInfo.number ?: "" + 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, triggerType, includeClientAuth), + verification_tokens = ConstellationStateStore.loadVerificationTokens(context) + ) +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/TelephonyInfoBuilder.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/TelephonyInfoBuilder.kt new file mode 100644 index 0000000000..62e4ada9b7 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/TelephonyInfoBuilder.kt @@ -0,0 +1,143 @@ +package org.microg.gms.constellation.proto.builders + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.os.Build +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.util.Base64 +import android.util.Log +import androidx.core.content.getSystemService +import org.microg.gms.constellation.proto.NetworkSignal +import org.microg.gms.constellation.proto.SimNetworkInfo +import org.microg.gms.constellation.proto.SimOperatorInfo +import org.microg.gms.constellation.proto.TelephonyInfo +import java.security.MessageDigest + +private const val TAG = "TelephonyInfoBuilder" + +@SuppressLint("HardwareIds") +operator fun TelephonyInfo.Companion.invoke(context: Context, subscriptionId: Int): TelephonyInfo { + val tm = context.getSystemService() + 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() + 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 ?: "" + ) + + return TelephonyInfo( + phone_type = phoneType, + group_id_level1 = targetTm?.groupIdLevel1 ?: "", + sim_info = simInfo, + network_info = networkInfo, + network_roaming = networkRoaming, + connectivity_state = connectivityState, + sms_capability = if (targetTm?.isSmsCapable == true) TelephonyInfo.SmsCapability.SMS_CAPABLE else TelephonyInfo.SmsCapability.SMS_NOT_CAPABLE, + sim_state = simState, + is_embedded = false + ) +} + +fun NetworkSignal.Companion.getList(context: Context): List { + val connectivityInfos = mutableListOf() + try { + val cm = context.getSystemService() + 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 { + val infos = mutableListOf() + try { + val sm = context.getSystemService() + val tm = context.getSystemService() + + 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 +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/CarrierIdVerifier.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/CarrierIdVerifier.kt new file mode 100644 index 0000000000..b7c138638f --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/CarrierIdVerifier.kt @@ -0,0 +1,84 @@ +package org.microg.gms.constellation.verification + +import android.content.Context +import android.os.Build +import android.telephony.TelephonyManager +import android.util.Log +import androidx.core.content.getSystemService +import org.microg.gms.constellation.proto.CarrierIdChallengeResponse +import org.microg.gms.constellation.proto.CarrierIdError +import org.microg.gms.constellation.proto.Challenge +import org.microg.gms.constellation.proto.ChallengeResponse + +private const val TAG = "CarrierIdVerifier" + +class CarrierIdVerifier(private val context: Context, private val subId: Int) { + fun verify(challenge: Challenge?): ChallengeResponse? { + val carrierChallenge = challenge?.carrier_id_challenge ?: return null + 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 appType = carrierChallenge.app_type + val authType = carrierChallenge.auth_type + + return try { + val telephonyManager = context.getSystemService() + ?: return failure( + CarrierIdError.CARRIER_ID_ERROR_NOT_SUPPORTED, + "TelephonyManager unavailable" + ) + val targetManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + telephonyManager.createForSubscriptionId(subId) + } else { + telephonyManager + } + + val response = targetManager.getIccAuthentication(appType, authType, 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" + ) + } + } + + private fun failure(status: CarrierIdError, message: String): ChallengeResponse { + Log.w(TAG, message) + return ChallengeResponse( + carrier_id_response = CarrierIdChallengeResponse( + isim_response = "", + carrier_id_error = status + ) + ) + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ChallengeProcessor.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ChallengeProcessor.kt new file mode 100644 index 0000000000..e731f5112d --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ChallengeProcessor.kt @@ -0,0 +1,152 @@ +package org.microg.gms.constellation.verification + +import android.content.Context +import android.telephony.SubscriptionInfo +import android.util.Log +import com.squareup.wire.GrpcException +import com.squareup.wire.GrpcStatus +import org.microg.gms.constellation.ConstellationStateStore +import org.microg.gms.constellation.RpcClient +import org.microg.gms.constellation.getState +import org.microg.gms.constellation.proto.ChallengeResponse +import org.microg.gms.constellation.proto.ProceedRequest +import org.microg.gms.constellation.proto.RequestHeader +import org.microg.gms.constellation.proto.RequestTrigger +import org.microg.gms.constellation.proto.Verification +import org.microg.gms.constellation.proto.VerificationMethod +import org.microg.gms.constellation.proto.builders.RequestBuildContext +import org.microg.gms.constellation.proto.builders.invoke + +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, + buildContext: RequestBuildContext, + verification: Verification, + ): Verification { + var currentVerification = verification + + for (attempt in 1..MAX_PROCEED_ROUNDS) { + if (currentVerification.getState() != Verification.State.PENDING) { + Log.d( + TAG, + "Verification state: ${currentVerification.getState()}. 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) { + 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 + + val challengeResponse: ChallengeResponse? = when { + challenge.type == VerificationMethod.TS43 -> Ts43Verifier( + context, + subId + ).verify(challenge.ts43_challenge) + + challenge.type == VerificationMethod.CARRIER_ID && challenge.ts43_challenge != null -> + Ts43Verifier(context, subId).verify(challenge.ts43_challenge) + + challenge.type == VerificationMethod.CARRIER_ID -> + CarrierIdVerifier(context, subId).verify(challenge) + + challenge.type == VerificationMethod.MT_SMS -> MtSmsVerifier( + context, + subId + ).verify(challenge.mt_challenge) + + challenge.type == VerificationMethod.MO_SMS -> MoSmsVerifier( + context, + subId + ).verify(challenge.mo_challenge) + + challenge.type == VerificationMethod.REGISTERED_SMS -> + RegisteredSmsVerifier(context, subId).verify(challenge.registered_sms_challenge) + + challenge.type == VerificationMethod.FLASH_CALL -> { + Log.w(TAG, "Flash call verification is unavailable on this build") + null + } + + else -> { + Log.w(TAG, "Unsupported verification method: ${challenge.type}") + null + } + } + + if (challengeResponse != null) { + Log.d(TAG, "Attempt $attempt: Challenge successfully solved. Proceeding...") + val proceedHeader = RequestHeader( + context, + sessionId, + buildContext, + RequestTrigger.Type.TRIGGER_API_CALL, + 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 + } else { + 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.w(TAG, "Exhausted all $MAX_PROCEED_ROUNDS proceed rounds, record is still pending.") + return currentVerification + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MoSmsVerifier.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MoSmsVerifier.kt new file mode 100644 index 0000000000..f9a6f6bc4a --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MoSmsVerifier.kt @@ -0,0 +1,210 @@ +package org.microg.gms.constellation.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.RequiresPermission +import androidx.core.content.getSystemService +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import org.microg.gms.constellation.proto.ChallengeResponse +import org.microg.gms.constellation.proto.MoChallenge +import org.microg.gms.constellation.proto.MOChallengeResponseData +import java.util.UUID +import kotlin.coroutines.resume + +private const val TAG = "MoSmsVerifier" +private const val ACTION_MO_SMS_SENT = "org.microg.gms.constellation.MO_SMS_SENT" + +class MoSmsVerifier(private val context: Context, private val subId: Int) { + suspend fun verify(challenge: MoChallenge?): ChallengeResponse? { + if (challenge == null) return null + if (challenge.proxy_number.isEmpty() || challenge.sms.isEmpty()) { + return failureResponse(MOChallengeResponseData.Status.FAILED_TO_SEND_MO) + } + if (context.checkCallingOrSelfPermission(Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "SEND_SMS permission missing") + return failureResponse(MOChallengeResponseData.Status.FAILED_TO_SEND_MO) + } + + val smsManager = resolveSmsManager() ?: return failureResponse( + if (subId != -1 && !isActiveSubscription(subId)) { + MOChallengeResponseData.Status.NO_ACTIVE_SUBSCRIPTION + } else { + MOChallengeResponseData.Status.NO_SMS_MANAGER + } + ) + + val port = challenge.data_sms_info?.destination_port ?: 0 + val isBinarySms = port > 0 + val action = if (isBinarySms) ACTION_MO_SMS_SENT else 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 ${challenge.proxy_number} with messageId: $messageId") + + return withTimeoutOrNull(30000) { + suspendCancellableCoroutine { continuation -> + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == action) { + val receivedId = intent.getStringExtra("message_id") + if (receivedId != messageId) return + + val resultCode = resultCode + val errorCode = intent.getIntExtra("errorCode", -1) + + Log.d(TAG, "MO SMS sent result: $resultCode, error: $errorCode") + + val status = when (resultCode) { + -1 -> MOChallengeResponseData.Status.COMPLETED + else -> MOChallengeResponseData.Status.FAILED_TO_SEND_MO + } + + try { + context.unregisterReceiver(this) + } catch (_: Exception) { + } + + if (continuation.isActive) { + continuation.resume( + ChallengeResponse( + mo_response = MOChallengeResponseData( + status = status, + sms_result_code = resultCode.toLong(), + sms_error_code = errorCode.toLong() + ) + ) + ) + } + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver( + receiver, + IntentFilter(action), + Context.RECEIVER_NOT_EXPORTED + ) + } else { + context.registerReceiver(receiver, IntentFilter(action)) + } + + continuation.invokeOnCancellation { + try { + context.unregisterReceiver(receiver) + } catch (_: Exception) { + } + } + + try { + if (isBinarySms) { + smsManager.sendDataMessage( + challenge.proxy_number, + null, + port.toShort(), + challenge.sms.encodeToByteArray(), + pendingIntent, + null + ) + } else { + smsManager.sendTextMessage( + challenge.proxy_number, + null, + challenge.sms, + pendingIntent, + null + ) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to initiate MO SMS send", e) + try { + context.unregisterReceiver(receiver) + } catch (_: Exception) { + } + if (continuation.isActive) { + continuation.resume( + ChallengeResponse( + mo_response = MOChallengeResponseData( + status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO + ) + ) + ) + } + } + } + } ?: ChallengeResponse( + mo_response = MOChallengeResponseData( + status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO + ) + ) + } + + private fun failureResponse(status: MOChallengeResponseData.Status): ChallengeResponse { + return ChallengeResponse( + mo_response = MOChallengeResponseData( + status = status + ) + ) + } + + @RequiresPermission(Manifest.permission.READ_PHONE_STATE) + private fun isActiveSubscription(subscriptionId: Int): Boolean { + if (subscriptionId == -1) return false + return try { + context.getSystemService()?.isActiveSubscriptionId(subscriptionId) + ?: false + } catch (e: Exception) { + Log.w(TAG, "Failed to query active subscription for $subscriptionId", e) + false + } + } + + private fun resolveSmsManager(): SmsManager? { + if (subId != -1 && !isActiveSubscription(subId)) { + return null + } + return try { + val manager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + context.getSystemService(SmsManager::class.java) + } else { + null + } + when { + subId != -1 && manager != null -> manager.createForSubscriptionId(subId) + manager != null -> manager + subId != -1 -> { + @Suppress("DEPRECATION") + SmsManager.getSmsManagerForSubscriptionId(subId) + } + + else -> { + @Suppress("DEPRECATION") + SmsManager.getDefault() + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to resolve SmsManager for subId: $subId", e) + null + } + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MtSmsVerifier.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MtSmsVerifier.kt new file mode 100644 index 0000000000..e757d60a07 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MtSmsVerifier.kt @@ -0,0 +1,95 @@ +package org.microg.gms.constellation.verification + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.provider.Telephony +import android.util.Log +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import org.microg.gms.constellation.proto.ChallengeResponse +import org.microg.gms.constellation.proto.MTChallenge +import org.microg.gms.constellation.proto.MTChallengeResponseData +import kotlin.coroutines.resume + +private const val TAG = "MtSmsVerifier" + +class MtSmsVerifier(private val context: Context, private val subId: Int) { + suspend fun verify(challenge: MTChallenge?): ChallengeResponse? { + val expectedBody = challenge?.sms?.takeIf { it.isNotEmpty() } ?: return null + + Log.d(TAG, "Waiting for MT SMS containing challenge string") + + val result = withTimeoutOrNull(300_000) { + suspendCancellableCoroutine?> { continuation -> + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + try { + if (intent.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) { + val receivedSubId = intent.getIntExtra( + "android.telephony.extra.SUBSCRIPTION_INDEX", + intent.getIntExtra("subscription", -1) + ) + if (subId != -1 && receivedSubId != -1 && receivedSubId != subId) { + return + } + val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) + for (msg in messages) { + val body = msg.messageBody + if (body != null && body.contains(expectedBody)) { + Log.d( + TAG, + "Matching MT SMS received from ${msg.originatingAddress}" + ) + context.unregisterReceiver(this) + if (continuation.isActive) { + continuation.resume( + Pair( + body, + msg.originatingAddress ?: "" + ) + ) + } + return + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Error in MT SMS receiver", e) + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver( + receiver, + IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION), + Context.RECEIVER_NOT_EXPORTED + ) + } else { + context.registerReceiver( + receiver, + IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) + ) + } + continuation.invokeOnCancellation { + try { + context.unregisterReceiver(receiver) + } catch (_: Exception) { + } + } + } + } + + return result?.let { (body, sender) -> + ChallengeResponse( + mt_response = MTChallengeResponseData( + sms = body, + sender = sender + ) + ) + } + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/RegisteredSmsVerifier.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/RegisteredSmsVerifier.kt new file mode 100644 index 0000000000..301527f0d7 --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/RegisteredSmsVerifier.kt @@ -0,0 +1,159 @@ +package org.microg.gms.constellation.verification + +import android.content.Context +import android.provider.Telephony +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.util.Log +import androidx.core.content.getSystemService +import okio.ByteString.Companion.toByteString +import org.microg.gms.constellation.VerificationSettingsPhenotypes +import org.microg.gms.constellation.proto.ChallengeResponse +import org.microg.gms.constellation.proto.RegisteredSmsChallenge +import org.microg.gms.constellation.proto.RegisteredSmsChallengeResponse +import org.microg.gms.constellation.proto.RegisteredSmsChallengeResponseItem +import org.microg.gms.constellation.proto.RegisteredSmsChallengeResponsePayload +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.concurrent.TimeUnit + +private const val TAG = "RegisteredSmsVerifier" + +class RegisteredSmsVerifier(private val context: Context, private val subId: Int) { + fun verify(challenge: RegisteredSmsChallenge?): ChallengeResponse? { + val expectedPayloads = challenge?.verified_senders + ?.map { it.phone_number_id.toByteArray() } + ?.filter { it.isNotEmpty() } + .orEmpty() + if (expectedPayloads.isEmpty()) return null + + val localNumbers = getLocalNumbers() + if (localNumbers.isEmpty()) { + Log.w(TAG, "No local phone numbers available for registered SMS verification") + return emptyResponse() + } + + 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() + + try { + val cursor = context.contentResolver.query( + Telephony.Sms.Inbox.CONTENT_URI, + arrayOf("date", "address", "body", "sub_id"), + "date > ?", + arrayOf(historyStart.toString()), + "date DESC" + ) ?: return emptyResponse() + + 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(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: SecurityException) { + Log.w(TAG, "SMS inbox access denied", e) + return emptyResponse() + } catch (e: Exception) { + Log.e(TAG, "Registered SMS verification failed", e) + return emptyResponse() + } + + return ChallengeResponse( + registered_sms_response = RegisteredSmsChallengeResponse(items = responseItems.distinct()) + ) + } + + private fun emptyResponse(): ChallengeResponse { + return ChallengeResponse( + registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList()) + ) + } + + private fun getLocalNumbers(targetSubId: Int = subId): List { + val numbers = linkedSetOf() + val subscriptionManager = + context.getSystemService() + val telephonyManager = context.getSystemService() + + try { + subscriptionManager?.activeSubscriptionInfoList.orEmpty().forEach { info -> + if (targetSubId != -1 && info.subscriptionId != targetSubId) return@forEach + if (android.os.Build.VERSION.SDK_INT >= android.os.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 (android.os.Build.VERSION.SDK_INT >= android.os.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)) + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/Ts43Verifier.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/Ts43Verifier.kt new file mode 100644 index 0000000000..4030817bcc --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/Ts43Verifier.kt @@ -0,0 +1,431 @@ +package org.microg.gms.constellation.verification + +import android.content.Context +import android.os.Build +import android.telephony.TelephonyManager +import android.util.Log +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.proto.ChallengeResponse +import org.microg.gms.constellation.proto.ClientChallengeResponse +import org.microg.gms.constellation.proto.OdsaOperation +import org.microg.gms.constellation.proto.ServerChallengeResponse +import org.microg.gms.constellation.proto.ServiceEntitlementRequest +import org.microg.gms.constellation.proto.Ts43Challenge +import org.microg.gms.constellation.proto.Ts43ChallengeResponse +import org.microg.gms.constellation.proto.Ts43ChallengeResponseError +import org.microg.gms.constellation.proto.Ts43ChallengeResponseStatus +import org.microg.gms.constellation.verification.ts43.EapAkaService +import org.microg.gms.constellation.verification.ts43.builder +import org.microg.gms.constellation.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 +import kotlin.text.ifEmpty + +private const val TAG = "Ts43Verifier" + +class Ts43Verifier(private val context: Context, private val subId: Int) { + + private class Ts43ApiException( + val errorCode: Ts43ChallengeResponseError.Code, + val httpStatus: Int, + val requestType: Ts43ChallengeResponseError.RequestType, + cause: Throwable? = null + ) : Exception(cause) + + private val httpHistory = mutableListOf() + + private val telephonyManager: TelephonyManager by lazy { + val tm = requireNotNull(context.getSystemService()) { + "TelephonyManager unavailable" + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && subId >= 0) { + tm.createForSubscriptionId(subId) + } else tm + } + + private val eapAkaService by lazy { EapAkaService(telephonyManager) } + + private fun errorCode(rawCode: Int): Ts43ChallengeResponseError.Code = + Ts43ChallengeResponseError.Code.fromValue(rawCode) + ?: Ts43ChallengeResponseError.Code.TS43_ERROR_CODE_UNSPECIFIED + + private val okHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .followRedirects(false) + .followSslRedirects(false) + .cookieJar(object : CookieJar { + private val cookieStore = mutableMapOf>() + override fun saveFromResponse(url: HttpUrl, cookies: List) { + 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 { + return cookieStore[url.host] + ?.filter { cookie -> cookie.matches(url) } + .orEmpty() + } + }) + .build() + } + + fun verify(challenge: Ts43Challenge?): ChallengeResponse? { + if (challenge == null) return null + httpHistory.clear() + + return try { + when { + challenge.client_challenge != null -> { + val op = challenge.client_challenge.operation + Log.d(TAG, "TS43: Client ODSA challenge route: ${op?.operation}") + if (op == null) { + buildFailureResponse( + challenge, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET + ) + } else { + val requestPayload = buildOdsaRequestPayload( + challenge.entitlement_url, + challenge.eap_aka_realm, + challenge.service_entitlement_request, + challenge.app_id + ) + if (requestPayload == null) { + buildFailureResponse( + challenge, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR + ) + } else { + val responseBody = performOdsaRequest( + challenge.entitlement_url, + op, + requestPayload, + challenge.app_id, + Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_GET_PHONE_NUMBER_API + ) + buildClientResponse(challenge, responseBody) + } + } + } + + challenge.server_challenge != null -> { + val op = challenge.server_challenge.operation + Log.d(TAG, "TS43: Server ODSA challenge route: ${op?.operation}") + if (op == null) { + buildFailureResponse( + challenge, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET + ) + } else { + val requestPayload = buildOdsaRequestPayload( + challenge.entitlement_url, + challenge.eap_aka_realm, + challenge.service_entitlement_request, + challenge.app_id + ) + if (requestPayload == null) { + buildFailureResponse( + challenge, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR + ) + } else { + val responseBody = performOdsaRequest( + challenge.entitlement_url, + op, + requestPayload, + challenge.app_id, + Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_ACQUIRE_TEMPORARY_TOKEN_API + ) + buildServerResponse(challenge, responseBody) + } + } + } + + else -> buildFailureResponse( + challenge, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET + ) + } + } catch (e: Ts43ApiException) { + Log.w(TAG, "TS43 API failure requestType=${e.requestType} http=${e.httpStatus}", e) + buildApiErrorResponse(challenge, e) + } catch (e: NullPointerException) { + Log.e(TAG, "TS43 null failure", e) + buildFailureResponse( + challenge, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR + ) + } catch (e: RuntimeException) { + Log.e(TAG, "TS43 runtime failure", e) + buildFailureResponse( + challenge, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_RUNTIME_ERROR + ) + } + } + + private fun performOdsaRequest( + entitlementUrl: String, + op: OdsaOperation, + req: ServiceEntitlementRequest?, + challengeAppId: String?, + requestType: Ts43ChallengeResponseError.RequestType + ): String { + val builder = op.builder(telephonyManager, req, currentAppIds(challengeAppId)) + 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( + 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 builder = req.builder(telephonyManager, eapId, currentAppIds(challengeAppId)) + + val initialUrl = builder.buildBaseUrl(entitlementUrl) + val postUrl = entitlementUrl + val userAgent = req.userAgent(context) + val acceptLanguage = Locale.getDefault().toLanguageTag().ifEmpty { "en-US" } + + // 1. Initial Identity Probe (GET) + 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" + + // 2. EAP Rounds Loop + 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 currentAppIds(challengeAppId: String? = null): List { + return listOfNotNull( + challengeAppId?.takeIf { it.isNotBlank() } + ).ifEmpty { + listOf("ap2014") + } + } + + private fun extractEapRelayPacket(body: String): String? { + return runCatching { JSONObject(body).optString("eap-relay-packet") } + .getOrNull() + ?.takeIf { it.isNotEmpty() } + } + + private fun extractAuthToken(body: String): String? { + runCatching { + JSONObject(body).optJSONObject("Token")?.optString("token") + }.getOrNull()?.takeIf { it.isNotEmpty() }?.let { return it } + + return runCatching { + val normalized = body + .replace("&", "&") + .replace("&amp;", "&") + .replace("\r\n", "\n") + val doc = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(InputSource(StringReader(normalized))) + val parms = doc.getElementsByTagName("parm") + for (i in 0 until parms.length) { + val node = parms.item(i) + val attrs = node.attributes ?: continue + val name = attrs.getNamedItem("name")?.nodeValue ?: continue + if (name == "token") { + return@runCatching attrs.getNamedItem("value")?.nodeValue + } + } + null + }.getOrNull()?.takeIf { it.isNotEmpty() } + } + + private fun buildServerResponse( + challenge: Ts43Challenge, + responseBody: String + ): ChallengeResponse { + return ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.ts43_type, + server_challenge_response = ServerChallengeResponse( + acquire_temporary_token_response = responseBody + ), + http_history = httpHistory.toList() + ) + ) + } + + private fun buildClientResponse( + challenge: Ts43Challenge, + responseBody: String + ): ChallengeResponse { + return ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.ts43_type, + client_challenge_response = ClientChallengeResponse( + get_phone_number_response = responseBody + ), + http_history = httpHistory.toList() + ) + ) + } + + private fun buildFailureResponse( + challenge: Ts43Challenge, + statusCode: Ts43ChallengeResponseStatus.Code + ): ChallengeResponse { + return ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.ts43_type, + status = Ts43ChallengeResponseStatus(status_code = statusCode), + http_history = httpHistory.toList() + ) + ) + } + + private fun buildApiErrorResponse( + challenge: Ts43Challenge, + error: Ts43ApiException + ): ChallengeResponse { + return ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.ts43_type, + status = Ts43ChallengeResponseStatus( + error = Ts43ChallengeResponseError( + error_code = error.errorCode, + http_status = error.httpStatus, + request_type = error.requestType + ) + ), + http_history = httpHistory.toList() + ) + ) + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/EapAkaService.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/EapAkaService.kt new file mode 100644 index 0000000000..9e271e5a4f --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/EapAkaService.kt @@ -0,0 +1,211 @@ +package org.microg.gms.constellation.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 + } +} diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/Fips186Prf.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/Fips186Prf.kt new file mode 100644 index 0000000000..0f2b138ebe --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/Fips186Prf.kt @@ -0,0 +1,108 @@ +package org.microg.gms.constellation.verification.ts43 + +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +object Fips186Prf { + fun deriveKeys( + identityBytes: ByteArray, + ik: ByteArray, + ck: ByteArray + ): Map { + 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) + ) + } +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/ServiceEntitlementExtension.kt b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/ServiceEntitlementExtension.kt new file mode 100644 index 0000000000..71f9e4c87b --- /dev/null +++ b/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/ServiceEntitlementExtension.kt @@ -0,0 +1,200 @@ +package org.microg.gms.constellation.verification.ts43 + +import android.content.Context +import android.telephony.TelephonyManager +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.microg.gms.constellation.proto.OdsaOperation +import org.microg.gms.constellation.proto.ServiceEntitlementRequest + +fun ServiceEntitlementRequest.builder( + telephonyManager: TelephonyManager, + eapId: String, + appIds: List = 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 = runCatching { + context.packageManager.getPackageInfo(context.packageName, 0).versionName.orEmpty() + }.getOrDefault("") + 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 = 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, + 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() + } +} diff --git a/play-services-constellation/src/main/proto/constellation.proto b/play-services-constellation/src/main/proto/constellation.proto new file mode 100644 index 0000000000..29a8bd027a --- /dev/null +++ b/play-services-constellation/src/main/proto/constellation.proto @@ -0,0 +1,1025 @@ +syntax = "proto3"; +option java_package = "org.microg.gms.constellation.proto"; + +package google.internal.communications.phonedeviceverification.v1; + +import "google/protobuf/timestamp.proto"; + +// Verification and consent RPCs. +service PhoneDeviceVerification { + rpc GetConsent(GetConsentRequest) returns (GetConsentResponse); + rpc SetConsent(SetConsentRequest) returns (SetConsentResponse); + rpc Sync(SyncRequest) returns (SyncResponse); + rpc Proceed(ProceedRequest) returns (ProceedResponse); +} + +// Read-only phone number retrieval RPC. +service PhoneNumber { + rpc GetVerifiedPhoneNumbers(GetVerifiedPhoneNumbersRequest) returns (GetVerifiedPhoneNumbersResponse); +} + +// Shared request and response messages. +// Header for each client request. +message RequestHeader { + ClientInfo client_info = 1; + ClientAuth client_auth = 2; + string session_id = 3; + RequestTrigger trigger = 4; +} +message ResponseHeader { + StatusProtoSimple status = 1; +} +message StatusProtoSimple { + int32 code = 1; +} +enum StatusCode { + STATUS_CODE_UNSPECIFIED = 0; + STATUS_CODE_OK = 1; + STATUS_CODE_ERROR = 2; +} +message ClientInfo { + DeviceID device_id = 1; + bytes client_public_key = 2; + string locale = 3; + int32 gmscore_version_number = 4; + string gmscore_version = 5; + int32 android_sdk_version = 6; + DroidGuardSignals droidguard_signals = 8; + repeated Experiment experiments = 9; + UserProfileType user_profile_type = 11; + repeated GaiaToken gaia_tokens = 12; + CountryInfo country_info = 13; + repeated NetworkSignal connectivity_infos = 14; + string model = 15; + string manufacturer = 16; + repeated SimOperatorInfo partial_sim_infos = 17; + DeviceType device_type = 18; + bool is_wearable_standalone = 19; + GaiaSignals gaia_signals = 20; + string device_fingerprint = 21; +} + +enum UserProfileType { + UNKNOWN_PROFILE_TYPE = 0; + REGULAR_USER = 1; + MANAGED_PROFILE = 2; +} + +enum DeviceType { + DEVICE_TYPE_UNKNOWN = 0; + DEVICE_TYPE_PHONE = 1; + DEVICE_TYPE_PHONE_GO = 2; + DEVICE_TYPE_TV = 3; + DEVICE_TYPE_WEARABLE = 4; + DEVICE_TYPE_AUTOMOTIVE = 5; + DEVICE_TYPE_BATTLESTAR = 6; + DEVICE_TYPE_CHROME_OS = 7; + DEVICE_TYPE_XR = 8; + DEVICE_TYPE_DESKTOP = 9; + DEVICE_TYPE_XR_PERIPHERAL = 10; +} +message ClientAuth { + DeviceID device_id = 1; + bytes signature = 2; + google.protobuf.Timestamp sign_timestamp = 3; +} +message CountryInfo { + repeated string sim_countries = 1; + repeated string network_countries = 2; +} +message DroidGuardSignals { + string droidguard_result = 1; + string droidguard_token = 2; +} +message DeviceID { + string iid_token = 1; + int64 primary_device_id = 2; + int64 user_serial = 3; + int64 gms_android_id = 4; +} +message RequestTrigger { + enum Type { + UNKNOWN = 0; + PERIODIC_CONSENT_CHECK = 1; + PERIODIC_REFRESH = 2; + SIM_STATE_CHANGED = 3; + GAIA_CHANGE_EVENT = 4; + USER_SETTINGS = 5; + DEBUG_SETTINGS = 6; + TRIGGER_API_CALL = 7; + REBOOT_CHECKER = 8; + SERVER_TRIGGER = 9; + FAILURE_RETRY = 10; + CONSENT_API_TRIGGER = 11; + PNVR_DEVICE_SETTINGS = 12; + } + Type type = 1; +} +message DroidGuardAttestation { + string droidguard_result = 1; + string droidguard_token = 2; +} +message Experiment { + string key = 1; + string value = 2; +} +message GaiaID { + string id = 1; +} +message GaiaToken { + string token = 1; +} +message GaiaSignalEntry { + string gaia_id = 1; + GaiaAccountSignalType signal_type = 2; + google.protobuf.Timestamp timestamp = 3; +} + +enum GaiaAccountSignalType { + GAIA_ACCOUNT_SIGNAL_UNSPECIFIED = 0; + GAIA_ACCOUNT_SIGNAL_AUTHENTICATED = 1; + GAIA_ACCOUNT_SIGNAL_UNAUTHENTICATED = 2; + GAIA_ACCOUNT_SIGNAL_REMOVED_WITH_GMS_AUTH_BROADCAST = 3; +} +message GaiaSignals { + repeated GaiaSignalEntry gaia_signals = 1; +} +message Status { + int32 code = 1; +} + +// Named API params used by verification RPCs. +message VerificationPolicy { + string policy_id = 1; + int64 max_verification_age_hours = 2; + IdTokenRequest id_token_request = 3; + string calling_package = 4; + repeated VerificationParam params = 5; +} + +// Parameters for ID token generation. +message IdTokenRequest { + string certificate_hash = 1; + string token_nonce = 2; +} +message VerificationParam { + string key = 1; + string value = 2; +} +message SimOperatorInfo { + string imsi_hash = 1; + + string sim_operator = 3; +} +message NetworkSignal { + enum Type { + TYPE_UNKNOWN = 0; + TYPE_WIFI = 1; + TYPE_MOBILE = 2; + } + enum Availability { + AVAILABILITY_UNKNOWN = 0; + AVAILABLE = 1; + NOT_AVAILABLE = 2; + } + enum State { + STATE_UNKNOWN = 0; + STATE_CONNECTING = 1; + STATE_CONNECTED = 2; + STATE_DISCONNECTING = 3; + STATE_DISCONNECTED = 4; + STATE_SUSPENDED = 5; + } + Type type = 1; + State state = 2; + Availability availability = 3; +} +message MobileOperatorInfo { + string country_code = 1; + uint64 nil_since_usec = 2; + string operator = 3; + uint32 nil_since_micros = 4; + string operator_name = 5; +} + +// Sync and proceed flow. +message SyncRequest { + repeated Verification verifications = 3; + RequestHeader header = 4; + repeated VerificationToken verification_tokens = 5; +} +message SyncResponse { + repeated VerificationResponse responses = 1; + ServerTimestamp next_sync_time = 2; + ResponseHeader header = 3; + DroidguardToken droidguard_token = 4; + repeated VerificationToken verification_tokens = 5; +} +message ProceedRequest { + Verification verification = 2; + ChallengeResponse challenge_response = 3; + RequestHeader header = 4; +} +message ProceedResponse { + Verification verification = 1; + ResponseHeader header = 2; + ServerTimestamp next_sync_time = 3; + DroidguardToken droidguard_token = 4; +} + +// Server time plus the time when the server wrote this message. +message ServerTimestamp { + google.protobuf.Timestamp timestamp = 1; + google.protobuf.Timestamp now = 2; +} +message VerificationToken { + bytes token = 1; + google.protobuf.Timestamp expiration_time = 2; +} + +// Verification state and challenge model. +message Verification { + enum State { + UNKNOWN = 0; + NONE = 1; + PENDING = 2; + VERIFIED = 3; + } + + enum Status { + STATUS_UNKNOWN = 0; + STATUS_NONE = 1; + STATUS_PENDING = 2; + STATUS_VERIFIED = 3; + STATUS_THROTTLED = 4; + STATUS_FAILED = 5; + STATUS_SKIPPED = 6; + STATUS_NOT_REQUIRED = 7; + STATUS_PHONE_NUMBER_ENTRY_REQUIRED = 8; + STATUS_INELIGIBLE = 9; + STATUS_DENIED = 10; + STATUS_NOT_IN_SERVICE = 11; + } + + VerificationAssociation association = 1; + Status status = 2; + oneof info { + VerificationInfo verification_info = 3; + PendingVerificationInfo pending_verification_info = 4; + UnverifiedInfo unverified_info = 9; + } + TelephonyInfo telephony_info = 5; + repeated Param api_params = 6; + ChallengePreference challenge_preference = 7; + VerificationPolicy structured_api_params = 8; +} +message VerificationResponse { + Verification verification = 1; + StatusProto error = 2; +} +message StatusProto { + int32 code = 1; + string message = 3; +} +message SIMSlotInfo { + int32 slot_index = 1; + int32 subscription_id = 2; +} +message SIMAssociation { + + message SIMInfo { + repeated string imsi = 1; + string sim_readable_number = 2; + repeated TelephonyPhoneNumber telephony_phone_number = 3; + string iccid = 4; + } + + message TelephonyPhoneNumber { + string phone_number = 1; + TelephonyPhoneNumberType phone_number_type = 2; + } + + SIMInfo sim_info = 1; + repeated GaiaToken gaia_tokens = 2; + SIMSlotInfo sim_slot = 4; +} +message GaiaAssociation { +} +message VerificationAssociation { + oneof association { + SIMAssociation sim = 1; + GaiaAssociation gaia = 2; + } +} + +// Information for a verified record. +message VerificationInfo { + string phone_number = 1; + google.protobuf.Timestamp verification_time = 2; + VerificationMethod challenge_method = 6; +} +message PendingVerificationInfo { + Challenge challenge = 2; +} + +// Challenge payloads and responses. +// Challenge issued by the server. +message Challenge { + ChallengeID challenge_id = 1; + VerificationMethod type = 2; + MoChallenge mo_challenge = 3; + CarrierIdChallenge carrier_id_challenge = 4; + ServerTimestamp expiry_time = 5; + MTChallenge mt_challenge = 6; + RegisteredSmsChallenge registered_sms_challenge = 7; + FlashCallChallenge flash_call_challenge = 8; + Ts43Challenge ts43_challenge = 12; +} + +// Carrier ID challenge used for SS7 traffic. +message CarrierIdChallenge { + string isim_request = 3; + int32 auth_type = 5; + int32 app_type = 6; +} +message MTChallenge { + string sms = 1; +} +message ChallengeID { + string id = 1; +} +message Capabilities { + string droidguard_result = 1; + string droidguard_token = 2; +} + +// MO SMS challenge. +message MoChallenge { + string proxy_number = 1; + DataSmsInfo data_sms_info = 3; + string sms = 4; + string polling_intervals = 5; + string sms_without_persisting = 6; +} +message DataSmsInfo { + int32 destination_port = 1; +} +message RegisteredSmsChallenge { + repeated PhoneNumberID verified_senders = 1; +} +message PhoneNumberID { + bytes phone_number_id = 1; +} +message FlashCallChallenge { + repeated PhoneRange phone_ranges = 1; + FlashCallState state = 2; + repeated ChallengeID previous_challenge_ids = 3; + repeated ChallengeResponse previous_challenge_responses = 4; + int64 millis_between_interceptions = 5; +} +message PhoneRange { + string phone_number_prefix = 1; + string phone_number_suffix = 2; + string country_code = 3; + string carrier = 4; +} + +enum FlashCallState { + FLASH_CALL_STATE_UNKNOWN = 0; + FLASH_CALL_STATE_PREPARING = 1; + FLASH_CALL_STATE_VERIFYING = 2; + FLASH_CALL_STATE_VERIFIED = 3; + FLASH_CALL_STATE_FAILED = 4; +} +message UnverifiedInfo { + enum Reason { + UNKNOWN_REASON = 0; + THROTTLED = 1; + FAILED = 2; + SKIPPED = 3; + NOT_REQUIRED = 4; + PHONE_NUMBER_ENTRY_REQUIRED = 5; + INELIGIBLE = 6; + DENIED = 7; + NOT_IN_SERVICE = 8; + } + Reason reason = 1; + google.protobuf.Timestamp retry_after_time = 2; + VerificationMethod challenge_method = 3; +} + +// Challenge response. +message ChallengeResponse { + MTChallengeResponseData mt_response = 1; + CarrierIdChallengeResponse carrier_id_response = 2; + MOChallengeResponseData mo_response = 3; + RegisteredSmsChallengeResponse registered_sms_response = 6; + FlashCallChallengeResponse flash_call_response = 7; + Ts43ChallengeResponse ts43_challenge_response = 9; +} +message MTChallengeResponseData { + string sms = 1; + string sender = 2; +} +message CarrierIdChallengeResponse { + string isim_response = 3; + CarrierIdError carrier_id_error = 4; +} + +enum CarrierIdError { + CARRIER_ID_ERROR_NO_ERROR = 0; + CARRIER_ID_ERROR_NOT_SUPPORTED = 1; + CARRIER_ID_ERROR_RETRY_ATTEMPT_EXCEEDED = 2; + CARRIER_ID_ERROR_NULL_RESPONSE = 3; + CARRIER_ID_ERROR_REFLECTION_ERROR = 4; + CARRIER_ID_ERROR_NO_SIM = 5; + CARRIER_ID_ERROR_UNABLE_TO_READ_SUBSCRIPTION = 6; + CARRIER_ID_ERROR_UNKNOWN_ERROR = 7; + CARRIER_ID_ERROR_ENTITLEMENT_SERVER_ERROR = 8; + CARRIER_ID_ERROR_JSON_PARSE_ERROR = 9; + CARRIER_ID_ERROR_INTERNAL_ERROR = 10; + CARRIER_ID_ERROR_INVALID_ARGUMENT = 11; +} +message MOChallengeResponseData { + enum Status { + UNKNOWN_STATUS = 0; + COMPLETED = 1; + FAILED_TO_SEND_MO = 2; + NO_ACTIVE_SUBSCRIPTION = 3; + NO_SMS_MANAGER = 4; + } + Status status = 1; + int64 sms_result_code = 2; + int64 sms_error_code = 3; +} +message RegisteredSmsChallengeResponse { + repeated RegisteredSmsChallengeResponseItem items = 1; +} +message RegisteredSmsChallengeResponseItem { + RegisteredSmsChallengeResponsePayload payload = 2; +} +message RegisteredSmsChallengeResponsePayload { + bytes payload = 1; +} +message Ts43ChallengeResponse { + Ts43Type ts43_type = 1; + ClientChallengeResponse client_challenge_response = 2; + ServerChallengeResponse server_challenge_response = 3; + Ts43ChallengeResponseStatus status = 4; + repeated string http_history = 5; +} +message Ts43ChallengeResponseStatus { + enum Code { + TS43_STATUS_UNSPECIFIED = 0; + TS43_STATUS_NOT_SUPPORTED = 1; + TS43_STATUS_CHALLENGE_NOT_SET = 2; + TS43_STATUS_INTERNAL_ERROR = 3; + TS43_STATUS_RUNTIME_ERROR = 4; + TS43_STATUS_JSON_PARSE_ERROR = 5; + } + + oneof result { + Code status_code = 1; + Ts43ChallengeResponseError error = 2; + } +} + +// The numeric 1..9 range is intentionally left generic because those values are +// overloaded across multiple TS.43 request stages. +message Ts43ChallengeResponseError { + enum Code { + TS43_ERROR_CODE_UNSPECIFIED = 0; + TS43_ERROR_CODE_1 = 1; + TS43_ERROR_CODE_2 = 2; + TS43_ERROR_CODE_3 = 3; + TS43_ERROR_CODE_4 = 4; + TS43_ERROR_CODE_5 = 5; + TS43_ERROR_CODE_6 = 6; + TS43_ERROR_CODE_7 = 7; + TS43_ERROR_CODE_8 = 8; + TS43_ERROR_CODE_9 = 9; + TS43_ERROR_CODE_PHONE_UNAVAILABLE = 10; + TS43_ERROR_CODE_INVALID_IMSI_OR_MCCMNC = 11; + TS43_ERROR_CODE_EAP_AKA_CHALLENGE_FAILED = 20; + TS43_ERROR_CODE_EAP_AKA_SYNCHRONIZATION_FAILURE = 21; + TS43_ERROR_CODE_EAP_AKA_AUTHENTICATION_FAILED = 22; + TS43_ERROR_CODE_CONNECTIVITY_FAILURE = 30; + TS43_ERROR_CODE_HTTP_ERROR_STATUS = 31; + TS43_ERROR_CODE_HTTP_MALFORMED_RESPONSE = 32; + TS43_ERROR_CODE_TEMPORARY_TOKEN_UNAVAILABLE = 60; + } + + enum RequestType { + TS43_REQUEST_TYPE_UNSPECIFIED = 0; + TS43_REQUEST_TYPE_AUTH_API = 1; + TS43_REQUEST_TYPE_GET_PHONE_NUMBER_API = 2; + TS43_REQUEST_TYPE_ACQUIRE_TEMPORARY_TOKEN_API = 3; + TS43_REQUEST_TYPE_SERVICE_ENTITLEMENT_API = 4; + } + + Code error_code = 1; + int32 http_status = 2; + RequestType request_type = 3; +} +message ServerChallengeResponse { + string acquire_temporary_token_response = 2; +} +message ClientChallengeResponse { + string get_phone_number_response = 2; +} +message FlashCallChallengeResponse { + enum Error { + NO_ERROR = 0; + UNSPECIFIED = 1; + TIMED_OUT = 2; + NETWORK_NOT_AVAILABLE = 3; + TOO_MANY_CALLS = 4; + CONCURRENT_REQUESTS = 5; + IN_ECBM = 6; + IN_EMERGENCY_CALL = 7; + PRECONDITIONS_FAILED = 8; + API_NOT_AVAILABLE = 9; + ERROR_PREVIOUS_INCOMING_CALL = 10; + STATE_NOT_PREPARING = 11; + STATE_NOT_VERIFYING = 12; + ERROR_PENDING_VERIFICATION = 13; + PROCEED_FAILED = 14; + INTERCEPTION_FAILED = 15; + } + string caller = 1; + Error error = 2; +} +message Ts43Type { + enum Integrator { + TS43_INTEGRATOR_UNSPECIFIED = 0; + JIO = 1; + TELUS = 2; + ERICSSON = 3; + HPE = 4; + TMO = 5; + TMO_SERVER = 6; + TELENOR = 7; + RCS_CIS_PROXY = 8; + MOBI_US = 9; + SFR = 10; + SASKTEL_CANADA = 11; + DT = 13; + DT_SERVER = 14; + NETLYNC = 17; + ORANGE_FRANCE = 18; + AMDOCS = 19; + } + Integrator integrator = 1; +} +message CellularNetworkEvent { + google.protobuf.Timestamp timestamp = 1; + bool mobile_data_enabled = 2; + bool airplane_mode_enabled = 3; + bool data_roaming_enabled = 4; + bool mobile_data_always_on = 5; + repeated NetworkState networks = 6; +} +message NetworkState { + repeated int32 types = 1; + bool available = 2; +} +message ServiceStateEvent { + google.protobuf.Timestamp timestamp = 1; + bool airplane_mode_enabled = 2; + bool mobile_data_enabled = 3; + int32 voice_registration_state = 4; + int32 data_registration_state = 5; + int32 voice_network_type = 6; + int32 data_network_type = 7; + int32 signal_strength = 8; +} +message SMSEvent { + google.protobuf.Timestamp timestamp = 1; + EventDirection direction = 2; + EventPhoneNumberType number_type = 3; +} +message CallEvent { + google.protobuf.Timestamp timestamp = 1; + EventDirection direction = 2; +} + +enum EventDirection { + UNKNOWN_DIRECTION = 0; + INCOMING = 1; + OUTGOING = 2; + MISSED = 3; +} + +enum EventPhoneNumberType { + UNKNOWN_TYPE = 0; + LONG_NUMBER = 1; + SHORT_CODE = 2; +} +message TelephonyInfo { + enum PhoneType { + PHONE_TYPE_UNKNOWN = 0; + PHONE_TYPE_GSM = 1; + PHONE_TYPE_CDMA = 2; + PHONE_TYPE_SIP = 3; + } + enum SimState { + SIM_STATE_UNKNOWN = 0; + SIM_STATE_NOT_READY = 1; + SIM_STATE_READY = 2; + } + enum RoamingState { + ROAMING_UNKNOWN = 0; + ROAMING_HOME = 1; + ROAMING_ROAMING = 2; + } + + enum ConnectivityState { + CONNECTIVITY_UNKNOWN = 0; + CONNECTIVITY_HOME = 1; + CONNECTIVITY_ROAMING = 2; + } + enum SmsCapability { + SMS_UNKNOWN = 0; + SMS_NOT_CAPABLE = 1; + SMS_CAPABLE = 2; + SMS_DEFAULT_CAPABILITY = 3; + SMS_RESTRICTED = 4; + } + enum ToggleState { + TOGGLE_UNKNOWN = 0; + TOGGLE_DISABLED = 1; + TOGGLE_ENABLED = 2; + } + enum ServiceState { + SERVICE_STATE_UNKNOWN = 0; + SERVICE_STATE_IN_SERVICE = 1; + SERVICE_STATE_OUT_OF_SERVICE = 2; + SERVICE_STATE_EMERGENCY_ONLY = 3; + SERVICE_STATE_POWER_OFF = 4; + } + + PhoneType phone_type = 1; + string group_id_level1 = 2; + SimNetworkInfo sim_info = 3; + SimNetworkInfo network_info = 4; + RoamingState network_roaming = 5; + ConnectivityState connectivity_state = 6; + SmsCapability sms_capability = 7; + int32 active_sub_count = 8; + int32 active_sub_count_max = 9; + ToggleState vowifi_state = 11; + ToggleState sms_no_confirmation_state = 12; + SimState sim_state = 13; + string device_id = 15; + ServiceState service_state = 16; + repeated CellularNetworkEvent cellular_events = 17; + repeated CallEvent call_events = 18; + repeated SMSEvent sms_events = 19; + repeated ServiceStateEvent service_events = 20; + bool is_embedded = 21; + ToggleState carrier_id_capability = 23; + int64 sim_carrier_id = 25; +} +message SimNetworkInfo { + string country_iso = 1; + string operator = 2; + string operator_name = 3; + int32 inactive_time_diff_ms = 4; +} +message DroidguardToken { + string token = 1; + google.protobuf.Timestamp ttl = 2; +} + +// Consent and Asterism messages. +message GetConsentRequest { + DeviceID device_id = 1; + repeated GaiaToken gaia_tokens = 2; + RequestHeader header = 4; + repeated VerificationToken verification_tokens = 5; + AsterismClient asterism_client = 6; + VerificationPolicy structured_api_params = 7; + bool force_refresh = 8; + string session_id = 9; + bool unknown_flag = 10; +} +message GetConsentResponse { + RcsConsent rcs_consent = 1; + ServerTimestamp next_check_time = 5; + ConsentState device_consent = 6; + repeated GaiaConsent gaia_consents = 8; + DroidguardToken droidguard_token = 9; + PermissionState permission_state = 10; +} + +// These values intentionally alias because the same field is reused across +// multiple consent surfaces. +enum FlowContext { + option allow_alias = true; + FLOW_CONTEXT_UNSPECIFIED = 0; + FLOW_CONTEXT_RCS_DEFAULT_ON_LEGAL_FYI = 0; + FLOW_CONTEXT_RCS_CONSENT = 1; + FLOW_CONTEXT_RCS_DEFAULT_ON_OUT_OF_BOX = 2; + FLOW_CONTEXT_RCS_SAMSUNG_UNFREEZE = 3; + FLOW_CONTEXT_RCS_DEFAULT_ON_LEGAL_FYI_IN_SETTINGS = 4; + FLOW_CONTEXT_RCS_UNKNOWN_5 = 5; +} +message SetConsentRequest { + RequestHeader header = 1; + RcsConsent rcs_consent = 3; + AsterismClient asterism_client = 4; + bytes audit_record = 6; + repeated Param api_params = 7; + OnDemandConsent on_demand_consent = 8; + FlowContext flow_context = 9; + oneof device_consent_oneof { + AsterismConsent device_consent = 10; + } +} +message SetConsentResponse { +} + +enum Consent { + CONSENT_UNKNOWN = 0; + CONSENTED = 1; + NO_CONSENT = 2; + EXPIRED = 3; +} + +// These values intentionally alias because RCS and device consent versions +// share the same field. +enum ConsentVersion { + option allow_alias = true; + CONSENT_VERSION_UNSPECIFIED = 0; + RCS_LEGAL_FYI = 0; + RCS_CONSENT = 1; + RCS_OUT_OF_BOX = 2; + PHONE_VERIFICATION_DEFAULT = 1; + PHONE_VERIFICATION_MESSAGES_CALLS_V1 = 2; + PHONE_VERIFICATION_INTL_SMS_CALLS = 3; + PHONE_VERIFICATION_REACHABILITY_INTL_SMS_CALLS = 4; +} +message AsterismConsent { + Consent consent = 1; + ConsentSource consent_source = 2; + ConsentVersion consent_version = 3; +} +message ConsentState { + Consent state = 2; +} + +// `flags` is still an opaque server-defined bitfield. +message PermissionState { + int32 flags = 1; + PermissionType type = 2; +} +enum PermissionType { + PERMISSION_TYPE_UNSPECIFIED = 0; + LEGACY_DPNV = 1; + PNVR = 2; + NOT_ALLOWED = 3; +} +message RcsConsent { + Consent consent = 2; + ConsentVersion consent_version = 3; +} +message GaiaConsent { + AsterismClient asterism_client = 1; + Consent consent = 2; + ConsentVersion consent_version = 3; +} +message OnDemandConsent { + Consent consent = 1; + GaiaToken gaia_token = 2; + string consent_variant = 3; + string trigger = 4; +} +enum ConsentSource { + SOURCE_UNSPECIFIED = 0; + ANDROID_DEVICE_SETTINGS = 1; + GAIA_USERNAME_RECOVERY = 2; + AOB_SETUP_WIZARD = 3; + MINUTEMAID_JS_BRIDGE = 4; + GAIA_WEB_JS_BRIDGE = 5; + AM_PROFILES = 6; + MEET_ON_DEMAND_CONSENT = 7; + GPAY_ON_DEMAND_CONSENT = 8; +} +enum AsterismClient { + UNKNOWN_CLIENT = 0; + CONSTELLATION = 1; + RCS = 2; + ONE_TIME_VERIFICATION = 3; +} + +// Read-only phone number RPC. +// Request message for GetVerifiedPhoneNumbers. +message GetVerifiedPhoneNumbersRequest { + enum PhoneNumberSelection { + SELECTION_UNSPECIFIED = 0; + CONSTELLATION = 1; + RCS = 2; + } + + string session_id = 1; + IIDTokenAuth iid_token_auth = 2; + repeated PhoneNumberSelection phone_number_selections = 3; + TokenOption token_option = 4; + string droidguard_result = 5; + ConsistencyOption consistency_option = 6; + RequestInfo request_info = 7; +} + +// Response message for GetVerifiedPhoneNumbers. +message GetVerifiedPhoneNumbersResponse { + repeated VerifiedPhoneNumber phone_numbers = 2; +} +message IIDTokenAuth { + string iid_token = 1; + bytes client_sign = 2; + google.protobuf.Timestamp sign_timestamp = 3; +} +message TokenOption { + string certificate_hash = 1; + string token_nonce = 2; + string package_name = 3; +} +message VerifiedPhoneNumber { + string phone_number = 1; + google.protobuf.Timestamp verification_time = 2; + string id_token = 3; + RcsState rcs_state = 4; +} + +enum RcsState { + STATE_UNSPECIFIED = 0; + ACTIVE = 1; +} + +enum TelephonyPhoneNumberType { + PHONE_NUMBER_SOURCE_UNSPECIFIED = 0; + PHONE_NUMBER_SOURCE_CARRIER = 1; + PHONE_NUMBER_SOURCE_UICC = 2; + PHONE_NUMBER_SOURCE_IMS = 3; +} + +// Shared helpers. +// Data consistency requirement for read-only RPCs. +message ConsistencyOption { + enum Consistency { + CONSISTENCY_UNSPECIFIED = 0; + STALE = 1; + STRONG = 2; + } + Consistency consistency = 1; +} + +// Request metadata for analytics and legacy policy attribution. +message RequestInfo { + string policy_id = 1; +} +message Param { + string key = 1; + string value = 2; +} + +// Client challenge preference. +message ChallengePreference { + repeated VerificationMethod capabilities = 1; + ChallengePreferenceMetadata metadata = 2; +} +message ChallengePreferenceMetadata { + string sms_signature = 2; +} +enum VerificationMethod { + UNKNOWN = 0; + MO_SMS = 1; + MT_SMS = 2; + CARRIER_ID = 3; + IMSI_LOOKUP = 5; + REGISTERED_SMS = 7; + FLASH_CALL = 8; + TS43 = 11; +} + +// Audit payloads. +message AuditToken { + AuditTokenMetadata metadata = 2; +} +message AuditTokenMetadata { + AuditUuid uuid = 1; +} +message AuditUuid { + int64 uuid_msb = 1; + int64 uuid_lsb = 2; +} +message AuditPayload { + AuditDeviceInfo device_info = 1; + AuditDeviceId device_id = 2; + AuditEventMetadata event_metadata = 10; +} +message AuditDeviceInfo { + oneof device_identifier { + string android_id_hash = 1; + string instance_id = 2; + } +} +message AuditDeviceId { + string android_id_hash = 1; +} +message AuditEventMetadata { + AuditEventType event_type = 1; + string session_id = 2; + int64 event_timestamp = 3; + AuditConsentDetails consent_details = 4; + string package_name = 5; + string tos_url = 6; + string tos_version = 7; + string language = 8; + string country = 9; + string gaia_id = 10; + AuditComponentInfo component_info = 11; + string trigger = 12; +} +enum AuditEventType { + EVENT_TYPE_UNSPECIFIED = 0; + ASTERISM_CONSENT_CHANGE = 1; + RCS_CONSENT_CHANGE = 2; + VERIFICATION_COMPLETE = 3; +} +message AuditConsentDetails { + repeated int32 sim_slot_ids = 1; +} +message AuditComponentInfo { + AuditComponentType component_type = 1; +} +enum AuditComponentType { + COMPONENT_UNSPECIFIED = 0; + ASTERISM_CONSTELLATION = 119; + ASTERISM_RCS = 120; +} +message AuditConsentState { + Consent state = 1; +} +enum AuditEventTypeValue { + EVENT_VALUE_UNSPECIFIED = 0; + ASTERISM_CLIENT_CONSENT_CHANGE = 187; +} + +// TS.43 challenge messages. +message Ts43Challenge { + Ts43Type ts43_type = 1; + string entitlement_url = 2; + ServiceEntitlementRequest service_entitlement_request = 3; + ClientChallenge client_challenge = 4; + ServerChallenge server_challenge = 5; + string app_id = 6; + string eap_aka_realm = 7; +} +message ServerChallenge { + OdsaOperation operation = 1; +} +message ClientChallenge { + OdsaOperation operation = 1; +} +message OdsaOperation { + string operation = 1; + int32 operation_type = 2; + repeated string operation_targets = 3; + string companion_terminal_id = 4; + string companion_terminal_vendor = 5; + string companion_terminal_model = 6; + string companion_terminal_software_version = 7; + string companion_terminal_friendly_name = 8; + string companion_terminal_service = 9; + string companion_terminal_iccid = 10; + string companion_terminal_eid = 11; + string terminal_iccid = 12; + string terminal_eid = 13; + string target_terminal_id = 14; + repeated string target_terminal_ids = 15; + string target_terminal_iccid = 16; + string target_terminal_eid = 17; + string target_terminal_serial_number = 18; + string target_terminal_model = 19; + string old_terminal_id = 20; + string old_terminal_iccid = 21; +} +message ServiceEntitlementRequest { + int32 notification_action = 1; + string entitlement_version = 2; + string temporary_token = 3; + string authentication_token = 4; + string terminal_id = 5; + string terminal_vendor = 6; + string terminal_model = 7; + string terminal_software_version = 8; + string app_name = 9; + string app_version = 10; + string notification_token = 11; + int32 configuration_version = 12; + string accept_content_type = 13; + string boost_type = 14; + string gid1 = 15; +} + diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 8fc2896bbc..47c2dc6553 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -59,6 +59,7 @@ dependencies { implementation project(':play-services-auth-base') implementation project(':play-services-auth') implementation project(':play-services-clearcut') + implementation project(':play-services-constellation') implementation project(':play-services-drive') implementation project(':play-services-games') implementation project(':play-services-maps') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 66038da7e2..1b7e601d97 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -179,6 +179,9 @@ android:name="android.hardware.camera" android:required="false" /> + + + + + + + + @@ -1218,7 +1228,6 @@ - diff --git a/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java b/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java index 327d8b1cc0..92df450a4f 100644 --- a/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java +++ b/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java @@ -246,8 +246,24 @@ public InstanceIdStore getStore() { } @PublicApi(exclude = true) - public String requestToken(String authorizedEntity, String scope, Bundle extras) { - throw new UnsupportedOperationException(); + public String requestToken(String authorizedEntity, String scope, Bundle extras) throws IOException { + if (extras == null) extras = new Bundle(); + + extras.putString(EXTRA_SENDER, authorizedEntity); + if (scope != null) { + extras.putString(EXTRA_SCOPE, scope); + } + + String actualSubtype = TextUtils.isEmpty(this.subtype) ? authorizedEntity : this.subtype; + + if (!extras.containsKey("legacy.register")) { + extras.putString(EXTRA_SUBSCIPTION, authorizedEntity); + extras.putString(EXTRA_SUBTYPE, actualSubtype); + extras.putString("X-" + EXTRA_SUBSCIPTION, authorizedEntity); + extras.putString("X-" + EXTRA_SUBTYPE, actualSubtype); + } + + return rpc.handleRegisterMessageResult(rpc.sendRegisterMessageBlocking(extras, getKeyPair())); } private synchronized KeyPair getKeyPair() { diff --git a/settings.gradle b/settings.gradle index 1ab99b9000..bc68c6a7b1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,6 +36,7 @@ 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' From 977afd11ee1855cd932e9da43413a04916a3502f Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:17:26 -0400 Subject: [PATCH 02/63] Refactor and small cleanups --- play-services-constellation/build.gradle | 30 +-------- play-services-constellation/core/build.gradle | 64 +++++++++++++++++++ .../core/src/main/AndroidManifest.xml | 26 ++++++++ .../gms/constellation/GetPnvCapabilities.kt | 30 +++++++++ .../gms/constellation/VerifyPhoneNumber.kt | 6 ++ .../gms/constellation/core}/AuthManager.kt | 2 +- .../core}/ConstellationApiService.kt | 2 +- .../core}/ConstellationStateStore.kt | 21 +++--- .../gms/constellation/core}/GServices.kt | 2 +- .../gms/constellation/core}/GetIidToken.kt | 2 +- .../constellation/core}/GetPnvCapabilities.kt | 15 ++--- .../core}/GetPnvCapabilitiesApiPhenotype.kt | 2 +- .../core}/GetVerifiedPhoneNumbers.kt | 17 +++-- .../constellation/core}/IidTokenPhenotypes.kt | 2 +- .../gms/constellation/core}/RpcClient.kt | 6 +- .../core}/VerificationMappings.kt | 10 +-- .../core}/VerificationSettingsPhenotypes.kt | 2 +- .../constellation/core}/VerifyPhoneNumber.kt | 37 +++++++---- .../core}/VerifyPhoneNumberApiPhenotypes.kt | 2 +- .../core}/proto/builders/ClientInfoBuilder.kt | 32 ++++------ .../core}/proto/builders/CommonBuilders.kt | 27 ++++---- .../core}/proto/builders/GaiaInfoBuilder.kt | 13 ++-- .../proto/builders/RequestBuildContext.kt | 6 +- .../proto/builders/SyncRequestBuilder.kt | 35 +++++----- .../proto/builders/TelephonyInfoBuilder.kt | 11 ++-- .../core}/verification/CarrierIdVerifier.kt | 10 +-- .../core}/verification/ChallengeProcessor.kt | 24 +++---- .../core}/verification/MoSmsVerifier.kt | 12 ++-- .../core}/verification/MtSmsVerifier.kt | 8 +-- .../verification/RegisteredSmsVerifier.kt | 14 ++-- .../core}/verification/Ts43Verifier.kt | 27 ++++---- .../core}/verification/ts43/EapAkaService.kt | 2 +- .../core}/verification/ts43/Fips186Prf.kt | 2 +- .../ts43/ServiceEntitlementExtension.kt | 6 +- .../src/main/proto/constellation.proto | 2 +- .../src/main/AndroidManifest.xml | 15 +---- .../gms/constellation/GetPnvCapabilities.kt | 28 +------- .../gms/constellation/PhoneNumberInfo.kt | 1 - .../gms/constellation/VerifyPhoneNumber.kt | 6 +- play-services-core/build.gradle | 2 +- .../src/main/AndroidManifest.xml | 10 --- settings.gradle | 1 + 42 files changed, 311 insertions(+), 261 deletions(-) create mode 100644 play-services-constellation/core/build.gradle create mode 100644 play-services-constellation/core/src/main/AndroidManifest.xml create mode 100644 play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt create mode 100644 play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/AuthManager.kt (98%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/ConstellationApiService.kt (99%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/ConstellationStateStore.kt (91%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/GServices.kt (95%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/GetIidToken.kt (96%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/GetPnvCapabilities.kt (93%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/GetPnvCapabilitiesApiPhenotype.kt (69%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/GetVerifiedPhoneNumbers.kt (89%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/IidTokenPhenotypes.kt (88%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/RpcClient.kt (88%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/VerificationMappings.kt (94%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/VerificationSettingsPhenotypes.kt (76%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/VerifyPhoneNumber.kt (90%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/VerifyPhoneNumberApiPhenotypes.kt (97%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/proto/builders/ClientInfoBuilder.kt (86%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/proto/builders/CommonBuilders.kt (69%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/proto/builders/GaiaInfoBuilder.kt (85%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/proto/builders/RequestBuildContext.kt (70%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/proto/builders/SyncRequestBuilder.kt (85%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/proto/builders/TelephonyInfoBuilder.kt (94%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/CarrierIdVerifier.kt (90%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/ChallengeProcessor.kt (88%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/MoSmsVerifier.kt (95%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/MtSmsVerifier.kt (94%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/RegisteredSmsVerifier.kt (92%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/Ts43Verifier.kt (94%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/ts43/EapAkaService.kt (99%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/ts43/Fips186Prf.kt (98%) rename play-services-constellation/{src/main/kotlin/org/microg/gms/constellation => core/src/main/kotlin/org/microg/gms/constellation/core}/verification/ts43/ServiceEntitlementExtension.kt (97%) rename play-services-constellation/{ => core}/src/main/proto/constellation.proto (99%) diff --git a/play-services-constellation/build.gradle b/play-services-constellation/build.gradle index 2301c04063..e0cc981be3 100644 --- a/play-services-constellation/build.gradle +++ b/play-services-constellation/build.gradle @@ -10,16 +10,6 @@ apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'signing' -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.squareup.wire:wire-gradle-plugin:4.9.3' - } -} -apply plugin: 'com.squareup.wire' - android { namespace "org.microg.gms.constellation" @@ -46,28 +36,12 @@ android { } } -wire { - kotlin { - rpcRole = 'client' - rpcCallStyle = 'suspending' - } -} - apply from: '../gradle/publish-android.gradle' -description = 'microG service implementation for play-services-constellation' +description = 'microG implementation of play-services-constellation' dependencies { - 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') - - api 'com.squareup.wire:wire-runtime:4.9.3' - api 'com.squareup.wire:wire-grpc-client:4.9.3' - api 'com.squareup.okhttp3:okhttp:4.12.0' + implementation project(':play-services-basement') kapt project(":safe-parcel-processor") } diff --git a/play-services-constellation/core/build.gradle b/play-services-constellation/core/build.gradle new file mode 100644 index 0000000000..c51c1f39ea --- /dev/null +++ b/play-services-constellation/core/build.gradle @@ -0,0 +1,64 @@ +/* + * 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: 'kotlin-parcelize' +apply plugin: 'kotlin-kapt' +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') + + api "com.squareup.wire:wire-runtime:$wireVersion" + api "com.squareup.wire:wire-grpc-client:$wireVersion" + api "com.squareup.okhttp3:okhttp:$okHttpVersion" +} diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..cbe65637d0 --- /dev/null +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt b/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt new file mode 100644 index 0000000000..3bd89b61e0 --- /dev/null +++ b/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt @@ -0,0 +1,30 @@ +package com.google.android.gms.constellation + +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 + } + } +} + +operator fun VerificationCapability.Companion.invoke( + verificationMethod: Int, + status: VerificationStatus +): VerificationCapability { + return VerificationCapability( + verificationMethod = verificationMethod, + statusValue = status.value + ) +} + +val VerificationCapability.status: VerificationStatus + get() = VerificationStatus.fromInt(statusValue) \ No newline at end of file diff --git a/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt new file mode 100644 index 0000000000..95c87e2688 --- /dev/null +++ b/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt @@ -0,0 +1,6 @@ +package com.google.android.gms.constellation + +import org.microg.gms.constellation.core.proto.VerificationMethod + +val VerifyPhoneNumberRequest.verificationMethods: List + get() = verificationMethodsValues.mapNotNull { VerificationMethod.fromValue(it) } \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/AuthManager.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt similarity index 98% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/AuthManager.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt index 148d585ed3..94b4bdb8a8 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/AuthManager.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.content.Context import android.util.Base64 diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationApiService.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt similarity index 99% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationApiService.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt index a6829f734b..f4ddbb4123 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationApiService.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.content.Context import android.os.Bundle diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationStateStore.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt similarity index 91% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationStateStore.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt index 5c6f07bd08..dc802dfacc 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/ConstellationStateStore.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.annotation.SuppressLint import android.content.Context @@ -6,14 +6,14 @@ import android.util.Base64 import androidx.core.content.edit import com.squareup.wire.Instant import okio.ByteString.Companion.toByteString -import org.microg.gms.constellation.proto.DroidguardToken -import org.microg.gms.constellation.proto.ProceedResponse -import org.microg.gms.constellation.proto.Consent -import org.microg.gms.constellation.proto.ConsentSource -import org.microg.gms.constellation.proto.ConsentVersion -import org.microg.gms.constellation.proto.ServerTimestamp -import org.microg.gms.constellation.proto.SyncResponse -import org.microg.gms.constellation.proto.VerificationToken +import org.microg.gms.constellation.core.proto.Consent +import org.microg.gms.constellation.core.proto.ConsentSource +import org.microg.gms.constellation.core.proto.ConsentVersion +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" @@ -119,7 +119,8 @@ object ConstellationStateStore { } fun loadPnvrNoticeConsent(context: Context): Consent { - val value = statePrefs(context).getInt(KEY_PNVR_NOTICE_CONSENT, Consent.CONSENT_UNKNOWN.value) + val value = + statePrefs(context).getInt(KEY_PNVR_NOTICE_CONSENT, Consent.CONSENT_UNKNOWN.value) return Consent.fromValue(value) ?: Consent.CONSENT_UNKNOWN } diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GServices.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GServices.kt similarity index 95% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GServices.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GServices.kt index ca5b6acbb9..a8c3c64481 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GServices.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GServices.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.content.ContentResolver import android.net.Uri diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetIidToken.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt similarity index 96% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetIidToken.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt index 08775f8b9f..9ba0e0b8af 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetIidToken.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.content.Context import android.util.Log diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilities.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilities.kt similarity index 93% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilities.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilities.kt index 38fec9f3e8..72d4617f2e 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilities.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilities.kt @@ -1,6 +1,5 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core -import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.os.Build @@ -9,7 +8,6 @@ import android.telephony.TelephonyManager import android.util.Base64 import android.util.Log import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission import androidx.core.content.getSystemService import com.google.android.gms.common.api.ApiMetadata import com.google.android.gms.common.api.Status @@ -19,6 +17,7 @@ import com.google.android.gms.constellation.SimCapability import com.google.android.gms.constellation.VerificationCapability import com.google.android.gms.constellation.VerificationStatus import com.google.android.gms.constellation.internal.IConstellationCallbacks +import com.google.android.gms.constellation.invoke import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.security.MessageDigest @@ -26,12 +25,6 @@ import java.security.MessageDigest private const val TAG = "GetPnvCapabilities" @SuppressLint("HardwareIds") -@RequiresPermission( - allOf = - [ - Manifest.permission.READ_PHONE_STATE, - "android.permission.READ_PRIVILEGED_PHONE_STATE"] -) @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) suspend fun handleGetPnvCapabilities( context: Context, @@ -67,7 +60,9 @@ suspend fun handleGetPnvCapabilities( VerificationCapability( 9, when { - !GetPnvCapabilitiesApiPhenotype.FPNV_ALLOWED_CARRIER_IDS.contains(carrierId) -> + !GetPnvCapabilitiesApiPhenotype.FPNV_ALLOWED_CARRIER_IDS.contains( + carrierId + ) -> VerificationStatus.UNSUPPORTED_CARRIER telephonyManager.simState != TelephonyManager.SIM_STATE_READY -> diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilitiesApiPhenotype.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilitiesApiPhenotype.kt similarity index 69% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilitiesApiPhenotype.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilitiesApiPhenotype.kt index 814fae3f1c..9a7f8b21bd 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetPnvCapabilitiesApiPhenotype.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilitiesApiPhenotype.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core object GetPnvCapabilitiesApiPhenotype { val FPNV_ALLOWED_CARRIER_IDS = ArrayList() diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt similarity index 89% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetVerifiedPhoneNumbers.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index b382bef5a8..5ab87e178c 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.content.Context import android.os.Bundle @@ -12,12 +12,12 @@ import com.google.android.gms.constellation.internal.IConstellationCallbacks import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString -import org.microg.gms.constellation.proto.GetVerifiedPhoneNumbersRequest -import org.microg.gms.constellation.proto.GetVerifiedPhoneNumbersRequest.PhoneNumberSelection -import org.microg.gms.constellation.proto.IIDTokenAuth -import org.microg.gms.constellation.proto.TokenOption -import org.microg.gms.constellation.proto.VerifiedPhoneNumber as RpcVerifiedPhoneNumber +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 java.util.UUID +import org.microg.gms.constellation.core.proto.VerifiedPhoneNumber as RpcVerifiedPhoneNumber private const val TAG = "GetVerifiedPhoneNumbers" @@ -79,7 +79,10 @@ internal suspend fun fetchVerifiedPhoneNumbers( } internal fun List.toVerifyPhoneNumberResponse(): VerifyPhoneNumberResponse { - return VerifyPhoneNumberResponse(map { it.toPhoneNumberVerification() }.toTypedArray(), Bundle.EMPTY) + return VerifyPhoneNumberResponse( + map { it.toPhoneNumberVerification() }.toTypedArray(), + Bundle.EMPTY + ) } private fun RpcVerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo { diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/IidTokenPhenotypes.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/IidTokenPhenotypes.kt similarity index 88% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/IidTokenPhenotypes.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/IidTokenPhenotypes.kt index a700f12f2f..984c902321 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/IidTokenPhenotypes.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/IidTokenPhenotypes.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core object IidTokenPhenotypes { const val ASTERISM_PROJECT_NUMBER = 496232013492 diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/RpcClient.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt similarity index 88% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/RpcClient.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt index f7a49733aa..57495656c3 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/RpcClient.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt @@ -1,12 +1,12 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import com.squareup.wire.GrpcClient import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import org.microg.gms.common.Constants -import org.microg.gms.constellation.proto.PhoneDeviceVerificationClient -import org.microg.gms.constellation.proto.PhoneNumberClient +import org.microg.gms.constellation.core.proto.PhoneDeviceVerificationClient +import org.microg.gms.constellation.core.proto.PhoneNumberClient private class AuthInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationMappings.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt similarity index 94% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationMappings.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt index 55bbdda27e..62255e1fc0 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationMappings.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt @@ -1,11 +1,11 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.os.Bundle import com.google.android.gms.constellation.PhoneNumberVerification -import org.microg.gms.constellation.proto.Param -import org.microg.gms.constellation.proto.UnverifiedInfo -import org.microg.gms.constellation.proto.Verification -import org.microg.gms.constellation.proto.VerificationMethod +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) { diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationSettingsPhenotypes.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationSettingsPhenotypes.kt similarity index 76% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationSettingsPhenotypes.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationSettingsPhenotypes.kt index c51f0a279e..f8deaa5a7f 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerificationSettingsPhenotypes.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationSettingsPhenotypes.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core object VerificationSettingsPhenotypes { const val A2P_SMS_SIGNAL_GRANULARITY_HRS = 1L diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt similarity index 90% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumber.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index d0dc3b314c..1d07ea48c3 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core import android.content.Context import android.os.Build @@ -18,13 +18,13 @@ import com.squareup.wire.GrpcException import com.squareup.wire.GrpcStatus import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.microg.gms.constellation.proto.SyncRequest -import org.microg.gms.constellation.proto.Verification -import org.microg.gms.constellation.proto.builders.RequestBuildContext -import org.microg.gms.constellation.proto.builders.buildImsiToSubscriptionInfoMap -import org.microg.gms.constellation.proto.builders.buildRequestContext -import org.microg.gms.constellation.proto.builders.invoke -import org.microg.gms.constellation.verification.ChallengeProcessor +import org.microg.gms.constellation.core.proto.SyncRequest +import org.microg.gms.constellation.core.proto.Verification +import org.microg.gms.constellation.core.proto.builders.RequestBuildContext +import org.microg.gms.constellation.core.proto.builders.buildImsiToSubscriptionInfoMap +import org.microg.gms.constellation.core.proto.builders.buildRequestContext +import org.microg.gms.constellation.core.proto.builders.invoke +import org.microg.gms.constellation.core.verification.ChallengeProcessor import java.util.UUID private const val TAG = "VerifyPhoneNumber" @@ -62,7 +62,7 @@ suspend fun handleVerifyPhoneNumberV1( targetedSims = emptyList(), silent = false, apiVersion = 2, - verificationTypes = emptyList() + verificationMethodsValues = emptyList() ) val policyId = bundle.getString("policy_id", "") val mode = bundle.getInt("verification_mode", 0) @@ -111,7 +111,7 @@ suspend fun handleVerifyPhoneNumberSingleUse( targetedSims = emptyList(), silent = false, apiVersion = 2, - verificationTypes = emptyList() + verificationMethodsValues = emptyList() ) handleVerifyPhoneNumberRequest( @@ -200,12 +200,20 @@ private suspend fun handleVerifyPhoneNumberRequest( Log.e(TAG, "verifyPhoneNumber failed", e) when (readCallbackMode) { ReadCallbackMode.LEGACY -> { - callbacks.onPhoneNumberVerified(Status.INTERNAL_ERROR, emptyList(), ApiMetadata.DEFAULT) + callbacks.onPhoneNumberVerified( + Status.INTERNAL_ERROR, + emptyList(), + ApiMetadata.DEFAULT + ) } ReadCallbackMode.NONE -> { if (legacyCallbackOnFullFlow) { - callbacks.onPhoneNumberVerified(Status.INTERNAL_ERROR, emptyList(), ApiMetadata.DEFAULT) + callbacks.onPhoneNumberVerified( + Status.INTERNAL_ERROR, + emptyList(), + ApiMetadata.DEFAULT + ) } else { callbacks.onPhoneNumberVerificationsCompleted( Status.INTERNAL_ERROR, @@ -300,7 +308,10 @@ private suspend fun executeSyncFlow( if (e.grpcStatus == GrpcStatus.PERMISSION_DENIED || e.grpcStatus == GrpcStatus.UNAUTHENTICATED ) { - Log.w(TAG, "Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache...") + Log.w( + TAG, + "Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache..." + ) ConstellationStateStore.clearDroidGuardToken(context) } throw e diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumberApiPhenotypes.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumberApiPhenotypes.kt similarity index 97% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumberApiPhenotypes.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumberApiPhenotypes.kt index 93faca2dd1..ceebfcb771 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/VerifyPhoneNumberApiPhenotypes.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumberApiPhenotypes.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation +package org.microg.gms.constellation.core object VerifyPhoneNumberApiPhenotypes { val PACKAGES_ALLOWED_TO_CALL = listOf( diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt similarity index 86% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/ClientInfoBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt index 99e224cb3d..bc97172d63 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt @@ -1,37 +1,34 @@ -package org.microg.gms.constellation.proto.builders +package org.microg.gms.constellation.core.proto.builders -import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import android.util.Log -import androidx.annotation.RequiresPermission import androidx.core.content.edit import androidx.core.content.getSystemService import com.google.android.gms.tasks.await import okio.ByteString.Companion.toByteString import org.microg.gms.common.Constants -import org.microg.gms.constellation.AuthManager -import org.microg.gms.constellation.ConstellationStateStore -import org.microg.gms.constellation.GServices -import org.microg.gms.constellation.proto.ClientInfo -import org.microg.gms.constellation.proto.CountryInfo -import org.microg.gms.constellation.proto.DeviceID -import org.microg.gms.constellation.proto.DeviceType -import org.microg.gms.constellation.proto.DroidGuardSignals -import org.microg.gms.constellation.proto.GaiaSignals -import org.microg.gms.constellation.proto.GaiaToken -import org.microg.gms.constellation.proto.NetworkSignal -import org.microg.gms.constellation.proto.SimOperatorInfo -import org.microg.gms.constellation.proto.UserProfileType +import org.microg.gms.constellation.core.AuthManager +import org.microg.gms.constellation.core.ConstellationStateStore +import org.microg.gms.constellation.core.GServices +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.GaiaToken +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.util.Locale private const val TAG = "ClientInfoBuilder" private const val PREFS_NAME = "constellation_prefs" -@RequiresPermission(Manifest.permission.GET_ACCOUNTS) @SuppressLint("HardwareIds") suspend operator fun ClientInfo.Companion.invoke(context: Context, iidToken: String): ClientInfo { return ClientInfo( @@ -43,7 +40,6 @@ suspend operator fun ClientInfo.Companion.invoke(context: Context, iidToken: Str ) } -@RequiresPermission(Manifest.permission.GET_ACCOUNTS) @SuppressLint("HardwareIds") suspend operator fun ClientInfo.Companion.invoke( context: Context, diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/CommonBuilders.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt similarity index 69% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/CommonBuilders.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt index b05e0dc40c..d2f6f1e0da 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/CommonBuilders.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt @@ -1,19 +1,17 @@ -package org.microg.gms.constellation.proto.builders +package org.microg.gms.constellation.core.proto.builders -import android.Manifest import android.content.Context import android.os.Bundle -import androidx.annotation.RequiresPermission import okio.ByteString.Companion.toByteString -import org.microg.gms.constellation.AuthManager -import org.microg.gms.constellation.proto.AuditToken -import org.microg.gms.constellation.proto.AuditTokenMetadata -import org.microg.gms.constellation.proto.AuditUuid -import org.microg.gms.constellation.proto.ClientInfo -import org.microg.gms.constellation.proto.DeviceID -import org.microg.gms.constellation.proto.Param -import org.microg.gms.constellation.proto.RequestHeader -import org.microg.gms.constellation.proto.RequestTrigger +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 { if (extras == null) return emptyList() @@ -21,7 +19,7 @@ fun Param.Companion.getList(extras: Bundle?): List { val ignoreKeys = setOf("consent_variant_key", "consent_trigger_key", "gaia_access_token") for (key in extras.keySet()) { if (key !in ignoreKeys) { - extras.get(key)?.toString()?.let { value -> + extras.getString(key)?.let { value -> params.add(Param(key = key, value_ = value)) } } @@ -41,7 +39,6 @@ fun AuditToken.Companion.generate(): AuditToken { ) } -@RequiresPermission(Manifest.permission.GET_ACCOUNTS) suspend operator fun RequestHeader.Companion.invoke( context: Context, sessionId: String, @@ -52,7 +49,7 @@ suspend operator fun RequestHeader.Companion.invoke( val authManager = if (includeClientAuth) AuthManager.get(context) else null val clientAuth = if (includeClientAuth) { val (signature, timestamp) = authManager!!.signIidToken(buildContext.iidToken) - org.microg.gms.constellation.proto.ClientAuth( + org.microg.gms.constellation.core.proto.ClientAuth( device_id = DeviceID(context, buildContext.iidToken), signature = signature.toByteString(), sign_timestamp = timestamp diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt similarity index 85% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/GaiaInfoBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt index dd2b34b42d..febce16e53 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt @@ -1,24 +1,21 @@ -package org.microg.gms.constellation.proto.builders +package org.microg.gms.constellation.core.proto.builders -import android.Manifest import android.accounts.AccountManager import android.content.Context import android.util.Log -import androidx.annotation.RequiresPermission import com.squareup.wire.Instant import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.microg.gms.constellation.proto.GaiaAccountSignalType -import org.microg.gms.constellation.proto.GaiaSignalEntry -import org.microg.gms.constellation.proto.GaiaSignals -import org.microg.gms.constellation.proto.GaiaToken +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 import java.security.MessageDigest import kotlin.math.abs import kotlin.math.absoluteValue private const val TAG = "GaiaInfoBuilder" -@RequiresPermission(Manifest.permission.GET_ACCOUNTS) operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? { val entries = mutableListOf() try { diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/RequestBuildContext.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt similarity index 70% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/RequestBuildContext.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt index 5ac41bba97..c581237c17 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/RequestBuildContext.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt @@ -1,8 +1,8 @@ -package org.microg.gms.constellation.proto.builders +package org.microg.gms.constellation.core.proto.builders import android.content.Context -import org.microg.gms.constellation.AuthManager -import org.microg.gms.constellation.proto.GaiaToken +import org.microg.gms.constellation.core.AuthManager +import org.microg.gms.constellation.core.proto.GaiaToken data class RequestBuildContext( val iidToken: String, diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt similarity index 85% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/SyncRequestBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt index 7d86e3964f..61bc46d639 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.proto.builders +package org.microg.gms.constellation.core.proto.builders import android.annotation.SuppressLint import android.content.Context @@ -10,22 +10,23 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import com.google.android.gms.constellation.VerifyPhoneNumberRequest -import org.microg.gms.constellation.ConstellationStateStore -import org.microg.gms.constellation.proto.ChallengePreference -import org.microg.gms.constellation.proto.ChallengePreferenceMetadata -import org.microg.gms.constellation.proto.IdTokenRequest -import org.microg.gms.constellation.proto.Param -import org.microg.gms.constellation.proto.RequestHeader -import org.microg.gms.constellation.proto.RequestTrigger -import org.microg.gms.constellation.proto.SIMAssociation -import org.microg.gms.constellation.proto.SIMSlotInfo -import org.microg.gms.constellation.proto.SyncRequest -import org.microg.gms.constellation.proto.TelephonyInfo -import org.microg.gms.constellation.proto.TelephonyPhoneNumberType -import org.microg.gms.constellation.proto.Verification -import org.microg.gms.constellation.proto.VerificationAssociation -import org.microg.gms.constellation.proto.VerificationParam -import org.microg.gms.constellation.proto.VerificationPolicy +import com.google.android.gms.constellation.verificationMethods +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" diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/TelephonyInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt similarity index 94% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/TelephonyInfoBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt index 62e4ada9b7..4bef0d7581 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/proto/builders/TelephonyInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.proto.builders +package org.microg.gms.constellation.core.proto.builders import android.annotation.SuppressLint import android.content.Context @@ -9,10 +9,10 @@ import android.telephony.TelephonyManager import android.util.Base64 import android.util.Log import androidx.core.content.getSystemService -import org.microg.gms.constellation.proto.NetworkSignal -import org.microg.gms.constellation.proto.SimNetworkInfo -import org.microg.gms.constellation.proto.SimOperatorInfo -import org.microg.gms.constellation.proto.TelephonyInfo +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" @@ -107,6 +107,7 @@ fun NetworkSignal.Companion.getList(context: Context): List { return connectivityInfos } +@SuppressLint("HardwareIds") fun SimOperatorInfo.Companion.getList(context: Context): List { val infos = mutableListOf() try { diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/CarrierIdVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt similarity index 90% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/CarrierIdVerifier.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt index b7c138638f..f7159238c1 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/CarrierIdVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt @@ -1,14 +1,14 @@ -package org.microg.gms.constellation.verification +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.core.content.getSystemService -import org.microg.gms.constellation.proto.CarrierIdChallengeResponse -import org.microg.gms.constellation.proto.CarrierIdError -import org.microg.gms.constellation.proto.Challenge -import org.microg.gms.constellation.proto.ChallengeResponse +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" diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt similarity index 88% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ChallengeProcessor.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index e731f5112d..a5775cdedb 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -1,21 +1,21 @@ -package org.microg.gms.constellation.verification +package org.microg.gms.constellation.core.verification import android.content.Context import android.telephony.SubscriptionInfo import android.util.Log import com.squareup.wire.GrpcException import com.squareup.wire.GrpcStatus -import org.microg.gms.constellation.ConstellationStateStore -import org.microg.gms.constellation.RpcClient -import org.microg.gms.constellation.getState -import org.microg.gms.constellation.proto.ChallengeResponse -import org.microg.gms.constellation.proto.ProceedRequest -import org.microg.gms.constellation.proto.RequestHeader -import org.microg.gms.constellation.proto.RequestTrigger -import org.microg.gms.constellation.proto.Verification -import org.microg.gms.constellation.proto.VerificationMethod -import org.microg.gms.constellation.proto.builders.RequestBuildContext -import org.microg.gms.constellation.proto.builders.invoke +import org.microg.gms.constellation.core.ConstellationStateStore +import org.microg.gms.constellation.core.RpcClient +import org.microg.gms.constellation.core.getState +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.RequestTrigger +import org.microg.gms.constellation.core.proto.Verification +import org.microg.gms.constellation.core.proto.VerificationMethod +import org.microg.gms.constellation.core.proto.builders.RequestBuildContext +import org.microg.gms.constellation.core.proto.builders.invoke object ChallengeProcessor { private const val TAG = "ChallengeProcessor" diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt similarity index 95% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MoSmsVerifier.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index f9a6f6bc4a..a5c914b595 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.verification +package org.microg.gms.constellation.core.verification import android.Manifest import android.app.PendingIntent @@ -11,18 +11,17 @@ import android.os.Build import android.telephony.SmsManager import android.telephony.SubscriptionManager import android.util.Log -import androidx.annotation.RequiresPermission import androidx.core.content.getSystemService import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull -import org.microg.gms.constellation.proto.ChallengeResponse -import org.microg.gms.constellation.proto.MoChallenge -import org.microg.gms.constellation.proto.MOChallengeResponseData +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.MO_SMS_SENT" +private const val ACTION_MO_SMS_SENT = "org.microg.gms.constellation.coreMO_SMS_SENT" class MoSmsVerifier(private val context: Context, private val subId: Int) { suspend fun verify(challenge: MoChallenge?): ChallengeResponse? { @@ -167,7 +166,6 @@ class MoSmsVerifier(private val context: Context, private val subId: Int) { ) } - @RequiresPermission(Manifest.permission.READ_PHONE_STATE) private fun isActiveSubscription(subscriptionId: Int): Boolean { if (subscriptionId == -1) return false return try { diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt similarity index 94% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MtSmsVerifier.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index e757d60a07..5d5cc838f7 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.verification +package org.microg.gms.constellation.core.verification import android.content.BroadcastReceiver import android.content.Context @@ -9,9 +9,9 @@ import android.provider.Telephony import android.util.Log import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull -import org.microg.gms.constellation.proto.ChallengeResponse -import org.microg.gms.constellation.proto.MTChallenge -import org.microg.gms.constellation.proto.MTChallengeResponseData +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 kotlin.coroutines.resume private const val TAG = "MtSmsVerifier" diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/RegisteredSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt similarity index 92% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/RegisteredSmsVerifier.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt index 301527f0d7..3e1ea8d37e 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/RegisteredSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.verification +package org.microg.gms.constellation.core.verification import android.content.Context import android.provider.Telephony @@ -7,12 +7,12 @@ import android.telephony.TelephonyManager import android.util.Log import androidx.core.content.getSystemService import okio.ByteString.Companion.toByteString -import org.microg.gms.constellation.VerificationSettingsPhenotypes -import org.microg.gms.constellation.proto.ChallengeResponse -import org.microg.gms.constellation.proto.RegisteredSmsChallenge -import org.microg.gms.constellation.proto.RegisteredSmsChallengeResponse -import org.microg.gms.constellation.proto.RegisteredSmsChallengeResponseItem -import org.microg.gms.constellation.proto.RegisteredSmsChallengeResponsePayload +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 diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/Ts43Verifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt similarity index 94% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/Ts43Verifier.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt index 4030817bcc..4066a5dffb 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/Ts43Verifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.verification +package org.microg.gms.constellation.core.verification import android.content.Context import android.os.Build @@ -13,25 +13,24 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONObject -import org.microg.gms.constellation.proto.ChallengeResponse -import org.microg.gms.constellation.proto.ClientChallengeResponse -import org.microg.gms.constellation.proto.OdsaOperation -import org.microg.gms.constellation.proto.ServerChallengeResponse -import org.microg.gms.constellation.proto.ServiceEntitlementRequest -import org.microg.gms.constellation.proto.Ts43Challenge -import org.microg.gms.constellation.proto.Ts43ChallengeResponse -import org.microg.gms.constellation.proto.Ts43ChallengeResponseError -import org.microg.gms.constellation.proto.Ts43ChallengeResponseStatus -import org.microg.gms.constellation.verification.ts43.EapAkaService -import org.microg.gms.constellation.verification.ts43.builder -import org.microg.gms.constellation.verification.ts43.userAgent +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 -import kotlin.text.ifEmpty private const val TAG = "Ts43Verifier" diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/EapAkaService.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaService.kt similarity index 99% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/EapAkaService.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaService.kt index 9e271e5a4f..47fae2cb31 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/EapAkaService.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/EapAkaService.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.verification.ts43 +package org.microg.gms.constellation.core.verification.ts43 import android.os.Build import android.telephony.TelephonyManager diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/Fips186Prf.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/Fips186Prf.kt similarity index 98% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/Fips186Prf.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/Fips186Prf.kt index 0f2b138ebe..618cb719cb 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/Fips186Prf.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/Fips186Prf.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.verification.ts43 +package org.microg.gms.constellation.core.verification.ts43 import java.security.MessageDigest import java.security.NoSuchAlgorithmException diff --git a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/ServiceEntitlementExtension.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt similarity index 97% rename from play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/ServiceEntitlementExtension.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt index 71f9e4c87b..69e7e846cb 100644 --- a/play-services-constellation/src/main/kotlin/org/microg/gms/constellation/verification/ts43/ServiceEntitlementExtension.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt @@ -1,11 +1,11 @@ -package org.microg.gms.constellation.verification.ts43 +package org.microg.gms.constellation.core.verification.ts43 import android.content.Context import android.telephony.TelephonyManager import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import org.microg.gms.constellation.proto.OdsaOperation -import org.microg.gms.constellation.proto.ServiceEntitlementRequest +import org.microg.gms.constellation.core.proto.OdsaOperation +import org.microg.gms.constellation.core.proto.ServiceEntitlementRequest fun ServiceEntitlementRequest.builder( telephonyManager: TelephonyManager, diff --git a/play-services-constellation/src/main/proto/constellation.proto b/play-services-constellation/core/src/main/proto/constellation.proto similarity index 99% rename from play-services-constellation/src/main/proto/constellation.proto rename to play-services-constellation/core/src/main/proto/constellation.proto index 29a8bd027a..93a10c652c 100644 --- a/play-services-constellation/src/main/proto/constellation.proto +++ b/play-services-constellation/core/src/main/proto/constellation.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -option java_package = "org.microg.gms.constellation.proto"; +option java_package = "org.microg.gms.constellation.core.proto"; package google.internal.communications.phonedeviceverification.v1; diff --git a/play-services-constellation/src/main/AndroidManifest.xml b/play-services-constellation/src/main/AndroidManifest.xml index bf92ddcaf3..670e962e03 100644 --- a/play-services-constellation/src/main/AndroidManifest.xml +++ b/play-services-constellation/src/main/AndroidManifest.xml @@ -1,17 +1,6 @@ diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt index 4598d98bb1..fe80003950 100644 --- a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt +++ b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt @@ -52,36 +52,12 @@ data class SimCapability @Constructor constructor( @SafeParcelable.Class data class VerificationCapability @Constructor constructor( @JvmField @Param(1) @Field(1) val verificationMethod: Int, - @JvmField @Param(2) @Field(2) val statusValue: Int // Enums are typically passed as Ints in SafeParcelable + @JvmField @Param(2) @Field(2) val statusValue: Int ) : AbstractSafeParcelable() { - constructor(verificationMethod: Int, status: VerificationStatus) : this( - verificationMethod, - status.value - ) - - val status: VerificationStatus - get() = VerificationStatus.fromInt(statusValue) - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) companion object { @JvmField val CREATOR = findCreator(VerificationCapability::class.java) } -} - -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 - } - } -} +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt index 459a05b3cd..f16110f7f9 100644 --- a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt +++ b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt @@ -7,7 +7,6 @@ import com.google.android.gms.common.internal.safeparcel.SafeParcelable import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Constructor import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param -import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter @SafeParcelable.Class data class PhoneNumberInfo @Constructor constructor( diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt index e2f159a44c..c2a0ef8115 100644 --- a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt +++ b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt @@ -8,7 +8,6 @@ import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Construc import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter -import org.microg.gms.constellation.proto.VerificationMethod @SafeParcelable.Class data class VerifyPhoneNumberRequest @Constructor constructor( @@ -19,11 +18,8 @@ data class VerifyPhoneNumberRequest @Constructor constructor( @JvmField @Param(5) @Field(5) val targetedSims: List, @JvmField @Param(6) @Field(6) val silent: Boolean, @JvmField @Param(7) @Field(7) val apiVersion: Int, - @JvmField @Param(8) @Field(8) val verificationTypes: List + @JvmField @Param(8) @Field(8) val verificationMethodsValues: List ) : AbstractSafeParcelable() { - val verificationMethods: List - get() = verificationTypes.mapNotNull { VerificationMethod.fromValue(it) } - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) companion object { diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 47c2dc6553..362e39c76a 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation project(':play-services-cast-core') implementation project(':play-services-cast-framework-core') implementation project(':play-services-conscrypt-provider-core') + implementation project(':play-services-constellation-core') implementation project(':play-services-cronet-core') implementation project(':play-services-droidguard-core') implementation project(':play-services-fido-core') @@ -59,7 +60,6 @@ dependencies { implementation project(':play-services-auth-base') implementation project(':play-services-auth') implementation project(':play-services-clearcut') - implementation project(':play-services-constellation') implementation project(':play-services-drive') implementation project(':play-services-games') implementation project(':play-services-maps') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 2d9e8447d6..a29fff819c 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -179,9 +179,6 @@ android:name="android.hardware.camera" android:required="false" /> - - - - - - - - diff --git a/settings.gradle b/settings.gradle index bc68c6a7b1..9fcbcea289 100644 --- a/settings.gradle +++ b/settings.gradle @@ -93,6 +93,7 @@ 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' From 08b36eba647b0c704aca1975251815efada997a5 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:10:07 -0400 Subject: [PATCH 03/63] Refactor `org.microg.gms.constellation.core.verification` --- .../core/verification/CarrierIdVerifier.kt | 119 ++++---- .../core/verification/ChallengeProcessor.kt | 38 +-- .../core/verification/MoSmsVerifier.kt | 287 +++++++++--------- .../core/verification/MtSmsVerifier.kt | 118 ++++--- .../verification/RegisteredSmsVerifier.kt | 243 ++++++++------- .../core/verification/Ts43Verifier.kt | 170 +++++------ 6 files changed, 468 insertions(+), 507 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt index f7159238c1..53c1bcc72d 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt @@ -12,73 +12,74 @@ import org.microg.gms.constellation.core.proto.ChallengeResponse private const val TAG = "CarrierIdVerifier" -class CarrierIdVerifier(private val context: Context, private val subId: Int) { - fun verify(challenge: Challenge?): ChallengeResponse? { - val carrierChallenge = challenge?.carrier_id_challenge ?: return null - 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" +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 appType = carrierChallenge.app_type - val authType = carrierChallenge.auth_type - - return try { - val telephonyManager = context.getSystemService() - ?: return failure( - CarrierIdError.CARRIER_ID_ERROR_NOT_SUPPORTED, - "TelephonyManager unavailable" - ) - val targetManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - telephonyManager.createForSubscriptionId(subId) - } else { - telephonyManager - } + val appType = carrierChallenge.app_type + val authType = carrierChallenge.auth_type - val response = targetManager.getIccAuthentication(appType, authType, 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( + return try { + val telephonyManager = context.getSystemService() + ?: return 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" + "TelephonyManager unavailable" ) + val targetManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + telephonyManager.createForSubscriptionId(subId) + } else { + telephonyManager } - } - private fun failure(status: CarrierIdError, message: String): ChallengeResponse { - Log.w(TAG, message) - return ChallengeResponse( - carrier_id_response = CarrierIdChallengeResponse( - isim_response = "", - carrier_id_error = status + val response = targetManager.getIccAuthentication(appType, authType, 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" ) } } + +private fun failure(status: CarrierIdError, message: String): ChallengeResponse { + Log.w(TAG, message) + return ChallengeResponse( + carrier_id_response = CarrierIdChallengeResponse( + isim_response = "", + carrier_id_error = status + ) + ) +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index a5775cdedb..ac79655766 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -70,35 +70,25 @@ object ChallengeProcessor { val info = imsiToInfoMap[challengeImsi] val subId = info?.subscriptionId ?: -1 - val challengeResponse: ChallengeResponse? = when { - challenge.type == VerificationMethod.TS43 -> Ts43Verifier( - context, - subId - ).verify(challenge.ts43_challenge) - - challenge.type == VerificationMethod.CARRIER_ID && challenge.ts43_challenge != null -> - Ts43Verifier(context, subId).verify(challenge.ts43_challenge) + 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 { + challenge.verifyCarrierId(context, subId) + } + } - challenge.type == VerificationMethod.CARRIER_ID -> - CarrierIdVerifier(context, subId).verify(challenge) + VerificationMethod.MT_SMS -> challenge.mt_challenge?.verify(context, subId) - challenge.type == VerificationMethod.MT_SMS -> MtSmsVerifier( - context, - subId - ).verify(challenge.mt_challenge) + VerificationMethod.MO_SMS -> challenge.mo_challenge?.verify(context, subId) - challenge.type == VerificationMethod.MO_SMS -> MoSmsVerifier( + VerificationMethod.REGISTERED_SMS -> challenge.registered_sms_challenge?.verify( context, subId - ).verify(challenge.mo_challenge) - - challenge.type == VerificationMethod.REGISTERED_SMS -> - RegisteredSmsVerifier(context, subId).verify(challenge.registered_sms_challenge) - - challenge.type == VerificationMethod.FLASH_CALL -> { - Log.w(TAG, "Flash call verification is unavailable on this build") - null - } + ) else -> { Log.w(TAG, "Unsupported verification method: ${challenge.type}") diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index a5c914b595..19fd2d0305 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -23,186 +23,177 @@ import kotlin.coroutines.resume private const val TAG = "MoSmsVerifier" private const val ACTION_MO_SMS_SENT = "org.microg.gms.constellation.coreMO_SMS_SENT" -class MoSmsVerifier(private val context: Context, private val subId: Int) { - suspend fun verify(challenge: MoChallenge?): ChallengeResponse? { - if (challenge == null) return null - if (challenge.proxy_number.isEmpty() || challenge.sms.isEmpty()) { - return failureResponse(MOChallengeResponseData.Status.FAILED_TO_SEND_MO) - } - if (context.checkCallingOrSelfPermission(Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) { - Log.w(TAG, "SEND_SMS permission missing") - return failureResponse(MOChallengeResponseData.Status.FAILED_TO_SEND_MO) - } +suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse { + if (proxy_number.isEmpty() || sms.isEmpty()) { + return ChallengeResponse(mo_response = MOChallengeResponseData(status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO)) + } + if (context.checkCallingOrSelfPermission(Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "SEND_SMS permission missing") + return ChallengeResponse(mo_response = MOChallengeResponseData(status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO)) + } - val smsManager = resolveSmsManager() ?: return failureResponse( - if (subId != -1 && !isActiveSubscription(subId)) { + val smsManager = resolveSmsManager(context, subId) ?: return ChallengeResponse( + mo_response = MOChallengeResponseData( + status = if (subId != -1 && !isActiveSubscription(context, subId)) { MOChallengeResponseData.Status.NO_ACTIVE_SUBSCRIPTION } else { MOChallengeResponseData.Status.NO_SMS_MANAGER } ) + ) + + val port = data_sms_info?.destination_port ?: 0 + val isBinarySms = 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 port = challenge.data_sms_info?.destination_port ?: 0 - val isBinarySms = port > 0 - val action = if (isBinarySms) ACTION_MO_SMS_SENT else 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) + ) - 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") + + return withTimeoutOrNull(30000) { + suspendCancellableCoroutine { continuation -> + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == action) { + val receivedId = intent.getStringExtra("message_id") + if (receivedId != messageId) return + + val resultCode = resultCode + val errorCode = intent.getIntExtra("errorCode", -1) + + Log.d(TAG, "MO SMS sent result: $resultCode, error: $errorCode") + + val status = when (resultCode) { + -1 -> MOChallengeResponseData.Status.COMPLETED + else -> MOChallengeResponseData.Status.FAILED_TO_SEND_MO + } - Log.d(TAG, "Sending MO SMS to ${challenge.proxy_number} with messageId: $messageId") - - return withTimeoutOrNull(30000) { - suspendCancellableCoroutine { continuation -> - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == action) { - val receivedId = intent.getStringExtra("message_id") - if (receivedId != messageId) return - - val resultCode = resultCode - val errorCode = intent.getIntExtra("errorCode", -1) - - Log.d(TAG, "MO SMS sent result: $resultCode, error: $errorCode") - - val status = when (resultCode) { - -1 -> MOChallengeResponseData.Status.COMPLETED - else -> MOChallengeResponseData.Status.FAILED_TO_SEND_MO - } - - try { - context.unregisterReceiver(this) - } catch (_: Exception) { - } - - if (continuation.isActive) { - continuation.resume( - ChallengeResponse( - mo_response = MOChallengeResponseData( - status = status, - sms_result_code = resultCode.toLong(), - sms_error_code = errorCode.toLong() - ) + try { + context.unregisterReceiver(this) + } catch (_: Exception) { + } + + if (continuation.isActive) { + continuation.resume( + ChallengeResponse( + mo_response = MOChallengeResponseData( + status = status, + sms_result_code = resultCode.toLong(), + sms_error_code = errorCode.toLong() ) ) - } + ) } } } + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver( - receiver, - IntentFilter(action), - Context.RECEIVER_NOT_EXPORTED - ) - } else { - context.registerReceiver(receiver, IntentFilter(action)) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver( + receiver, + IntentFilter(action), + Context.RECEIVER_NOT_EXPORTED + ) + } else { + context.registerReceiver(receiver, IntentFilter(action)) + } - continuation.invokeOnCancellation { - try { - context.unregisterReceiver(receiver) - } catch (_: Exception) { - } + continuation.invokeOnCancellation { + try { + context.unregisterReceiver(receiver) + } catch (_: Exception) { } + } + try { + if (isBinarySms) { + 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) try { - if (isBinarySms) { - smsManager.sendDataMessage( - challenge.proxy_number, - null, - port.toShort(), - challenge.sms.encodeToByteArray(), - pendingIntent, - null - ) - } else { - smsManager.sendTextMessage( - challenge.proxy_number, - null, - challenge.sms, - pendingIntent, - null - ) - } - } catch (e: Exception) { - Log.e(TAG, "Failed to initiate MO SMS send", e) - try { - context.unregisterReceiver(receiver) - } catch (_: Exception) { - } - if (continuation.isActive) { - continuation.resume( - ChallengeResponse( - mo_response = MOChallengeResponseData( - status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO - ) + context.unregisterReceiver(receiver) + } catch (_: Exception) { + } + if (continuation.isActive) { + continuation.resume( + ChallengeResponse( + mo_response = MOChallengeResponseData( + status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO ) ) - } + ) } } - } ?: ChallengeResponse( - mo_response = MOChallengeResponseData( - status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO - ) + } + } ?: ChallengeResponse( + mo_response = MOChallengeResponseData( + status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO ) - } + ) +} - private fun failureResponse(status: MOChallengeResponseData.Status): ChallengeResponse { - return ChallengeResponse( - mo_response = MOChallengeResponseData( - status = status - ) - ) +private fun isActiveSubscription(context: Context, subscriptionId: Int): Boolean { + if (subscriptionId == -1) return false + return try { + context.getSystemService()?.isActiveSubscriptionId(subscriptionId) + ?: false + } catch (e: Exception) { + Log.w(TAG, "Failed to query active subscription for $subscriptionId", e) + false } +} - private fun isActiveSubscription(subscriptionId: Int): Boolean { - if (subscriptionId == -1) return false - return try { - context.getSystemService()?.isActiveSubscriptionId(subscriptionId) - ?: false - } catch (e: Exception) { - Log.w(TAG, "Failed to query active subscription for $subscriptionId", e) - false - } +private fun resolveSmsManager(context: Context, subId: Int): SmsManager? { + if (subId != -1 && !isActiveSubscription(context, subId)) { + return null } - - private fun resolveSmsManager(): SmsManager? { - if (subId != -1 && !isActiveSubscription(subId)) { - return null + return try { + val manager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + context.getSystemService(SmsManager::class.java) + } else { + null } - return try { - val manager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - context.getSystemService(SmsManager::class.java) - } else { - null + when { + subId != -1 && manager != null -> manager.createForSubscriptionId(subId) + manager != null -> manager + subId != -1 -> { + @Suppress("DEPRECATION") + SmsManager.getSmsManagerForSubscriptionId(subId) } - when { - subId != -1 && manager != null -> manager.createForSubscriptionId(subId) - manager != null -> manager - subId != -1 -> { - @Suppress("DEPRECATION") - SmsManager.getSmsManagerForSubscriptionId(subId) - } - else -> { - @Suppress("DEPRECATION") - SmsManager.getDefault() - } + else -> { + @Suppress("DEPRECATION") + SmsManager.getDefault() } - } catch (e: Exception) { - Log.e(TAG, "Failed to resolve SmsManager for subId: $subId", e) - null } + } catch (e: Exception) { + Log.e(TAG, "Failed to resolve SmsManager for subId: $subId", e) + null } } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index 5d5cc838f7..4ad7e84e6a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -16,80 +16,78 @@ import kotlin.coroutines.resume private const val TAG = "MtSmsVerifier" -class MtSmsVerifier(private val context: Context, private val subId: Int) { - suspend fun verify(challenge: MTChallenge?): ChallengeResponse? { - val expectedBody = challenge?.sms?.takeIf { it.isNotEmpty() } ?: return null +suspend fun MTChallenge.verify(context: Context, subId: Int): ChallengeResponse? { + val expectedBody = sms.takeIf { it.isNotEmpty() } ?: return null - Log.d(TAG, "Waiting for MT SMS containing challenge string") + Log.d(TAG, "Waiting for MT SMS containing challenge string") - val result = withTimeoutOrNull(300_000) { - suspendCancellableCoroutine?> { continuation -> - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - try { - if (intent.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) { - val receivedSubId = intent.getIntExtra( - "android.telephony.extra.SUBSCRIPTION_INDEX", - intent.getIntExtra("subscription", -1) - ) - if (subId != -1 && receivedSubId != -1 && receivedSubId != subId) { - return - } - val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) - for (msg in messages) { - val body = msg.messageBody - if (body != null && body.contains(expectedBody)) { - Log.d( - TAG, - "Matching MT SMS received from ${msg.originatingAddress}" - ) - context.unregisterReceiver(this) - if (continuation.isActive) { - continuation.resume( - Pair( - body, - msg.originatingAddress ?: "" - ) + val result = withTimeoutOrNull(300_000) { + suspendCancellableCoroutine?> { continuation -> + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + try { + if (intent.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) { + val receivedSubId = intent.getIntExtra( + "android.telephony.extra.SUBSCRIPTION_INDEX", + intent.getIntExtra("subscription", -1) + ) + if (subId != -1 && receivedSubId != -1 && receivedSubId != subId) { + return + } + val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) + for (msg in messages) { + val body = msg.messageBody + if (body != null && body.contains(expectedBody)) { + Log.d( + TAG, + "Matching MT SMS received from ${msg.originatingAddress}" + ) + context.unregisterReceiver(this) + if (continuation.isActive) { + continuation.resume( + Pair( + body, + msg.originatingAddress ?: "" ) - } - return + ) } + return } } - } catch (e: Exception) { - Log.e(TAG, "Error in MT SMS receiver", e) } + } catch (e: Exception) { + Log.e(TAG, "Error in MT SMS receiver", e) } } + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver( - receiver, - IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION), - Context.RECEIVER_NOT_EXPORTED - ) - } else { - context.registerReceiver( - receiver, - IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) - ) - } - continuation.invokeOnCancellation { - try { - context.unregisterReceiver(receiver) - } catch (_: Exception) { - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver( + receiver, + IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION), + Context.RECEIVER_NOT_EXPORTED + ) + } else { + context.registerReceiver( + receiver, + IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) + ) + } + continuation.invokeOnCancellation { + try { + context.unregisterReceiver(receiver) + } catch (_: Exception) { } } } + } - return result?.let { (body, sender) -> - ChallengeResponse( - mt_response = MTChallengeResponseData( - sms = body, - sender = sender - ) + return result?.let { (body, sender) -> + ChallengeResponse( + mt_response = MTChallengeResponseData( + sms = body, + sender = sender ) - } + ) } } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt index 3e1ea8d37e..ceb4395bb1 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt @@ -19,141 +19,140 @@ import java.util.concurrent.TimeUnit private const val TAG = "RegisteredSmsVerifier" -class RegisteredSmsVerifier(private val context: Context, private val subId: Int) { - fun verify(challenge: RegisteredSmsChallenge?): ChallengeResponse? { - val expectedPayloads = challenge?.verified_senders - ?.map { it.phone_number_id.toByteArray() } - ?.filter { it.isNotEmpty() } - .orEmpty() - if (expectedPayloads.isEmpty()) return null - - val localNumbers = getLocalNumbers() - if (localNumbers.isEmpty()) { - Log.w(TAG, "No local phone numbers available for registered SMS verification") - return emptyResponse() - } +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 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() - - try { - val cursor = context.contentResolver.query( - Telephony.Sms.Inbox.CONTENT_URI, - arrayOf("date", "address", "body", "sub_id"), - "date > ?", - arrayOf(historyStart.toString()), - "date DESC" - ) ?: return emptyResponse() - - 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(messageSubId).ifEmpty { localNumbers } - } + 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() + + 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() + ) + ) - 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() - ) + 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: SecurityException) { - Log.w(TAG, "SMS inbox access denied", e) - return emptyResponse() - } catch (e: Exception) { - Log.e(TAG, "Registered SMS verification failed", e) - return emptyResponse() } - - return ChallengeResponse( - registered_sms_response = RegisteredSmsChallengeResponse(items = responseItems.distinct()) - ) + } catch (e: SecurityException) { + Log.w(TAG, "SMS inbox access denied", e) + return ChallengeResponse(registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList())) + } catch (e: Exception) { + Log.e(TAG, "Registered SMS verification failed", e) + return ChallengeResponse(registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList())) } - private fun emptyResponse(): ChallengeResponse { - return ChallengeResponse( - registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList()) - ) - } + return ChallengeResponse( + registered_sms_response = RegisteredSmsChallengeResponse(items = responseItems.distinct()) + ) +} - private fun getLocalNumbers(targetSubId: Int = subId): List { - val numbers = linkedSetOf() - val subscriptionManager = - context.getSystemService() - val telephonyManager = context.getSystemService() - - try { - subscriptionManager?.activeSubscriptionInfoList.orEmpty().forEach { info -> - if (targetSubId != -1 && info.subscriptionId != targetSubId) return@forEach - if (android.os.Build.VERSION.SDK_INT >= android.os.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 (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - telephonyManager?.createForSubscriptionId(info.subscriptionId) - } else { - telephonyManager - } - numbers += targetManager?.line1Number.orEmpty() - numbers += info.number.orEmpty() +private fun getLocalNumbers(context: Context, targetSubId: Int): List { + val numbers = linkedSetOf() + val subscriptionManager = + context.getSystemService() + val telephonyManager = context.getSystemService() + + try { + subscriptionManager?.activeSubscriptionInfoList.orEmpty().forEach { info -> + if (targetSubId != -1 && info.subscriptionId != targetSubId) return@forEach + if (android.os.Build.VERSION.SDK_INT >= android.os.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 + ) } - } catch (e: Exception) { - Log.w(TAG, "Unable to collect local phone numbers", e) + val targetManager = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + telephonyManager?.createForSubscriptionId(info.subscriptionId) + } else { + telephonyManager + } + numbers += targetManager?.line1Number.orEmpty() + numbers += info.number.orEmpty() } - - return numbers.filter { it.isNotBlank() }.distinct() + } catch (e: Exception) { + Log.w(TAG, "Unable to collect local phone numbers", e) } - 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() - } + return numbers.filter { it.isNotBlank() }.distinct() +} - private fun hashUtf8(value: String): ByteArray { - return MessageDigest.getInstance("SHA-512") - .digest(value.toByteArray(StandardCharsets.UTF_8)) - } +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)) } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt index 4066a5dffb..ebaa6733ef 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt @@ -34,15 +34,12 @@ import javax.xml.parsers.DocumentBuilderFactory private const val TAG = "Ts43Verifier" -class Ts43Verifier(private val context: Context, private val subId: Int) { - - private class Ts43ApiException( - val errorCode: Ts43ChallengeResponseError.Code, - val httpStatus: Int, - val requestType: Ts43ChallengeResponseError.RequestType, - cause: Throwable? = null - ) : Exception(cause) +fun Ts43Challenge.verify(context: Context, subId: Int): ChallengeResponse { + val verifier = InternalTs43Verifier(context, subId) + return verifier.verify(this) +} +private class InternalTs43Verifier(private val context: Context, private val subId: Int) { private val httpHistory = mutableListOf() private val telephonyManager: TelephonyManager by lazy { @@ -56,41 +53,7 @@ class Ts43Verifier(private val context: Context, private val subId: Int) { private val eapAkaService by lazy { EapAkaService(telephonyManager) } - private fun errorCode(rawCode: Int): Ts43ChallengeResponseError.Code = - Ts43ChallengeResponseError.Code.fromValue(rawCode) - ?: Ts43ChallengeResponseError.Code.TS43_ERROR_CODE_UNSPECIFIED - - private val okHttpClient by lazy { - OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .followRedirects(false) - .followSslRedirects(false) - .cookieJar(object : CookieJar { - private val cookieStore = mutableMapOf>() - override fun saveFromResponse(url: HttpUrl, cookies: List) { - 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 { - return cookieStore[url.host] - ?.filter { cookie -> cookie.matches(url) } - .orEmpty() - } - }) - .build() - } - - fun verify(challenge: Ts43Challenge?): ChallengeResponse? { - if (challenge == null) return null + fun verify(challenge: Ts43Challenge): ChallengeResponse { httpHistory.clear() return try { @@ -123,7 +86,15 @@ class Ts43Verifier(private val context: Context, private val subId: Int) { challenge.app_id, Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_GET_PHONE_NUMBER_API ) - buildClientResponse(challenge, responseBody) + ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.ts43_type, + client_challenge_response = ClientChallengeResponse( + get_phone_number_response = responseBody + ), + http_history = httpHistory.toList() + ) + ) } } } @@ -156,7 +127,15 @@ class Ts43Verifier(private val context: Context, private val subId: Int) { challenge.app_id, Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_ACQUIRE_TEMPORARY_TOKEN_API ) - buildServerResponse(challenge, responseBody) + ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.ts43_type, + server_challenge_response = ServerChallengeResponse( + acquire_temporary_token_response = responseBody + ), + http_history = httpHistory.toList() + ) + ) } } } @@ -168,7 +147,19 @@ class Ts43Verifier(private val context: Context, private val subId: Int) { } } catch (e: Ts43ApiException) { Log.w(TAG, "TS43 API failure requestType=${e.requestType} http=${e.httpStatus}", e) - buildApiErrorResponse(challenge, e) + ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.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) buildFailureResponse( @@ -366,36 +357,6 @@ class Ts43Verifier(private val context: Context, private val subId: Int) { }.getOrNull()?.takeIf { it.isNotEmpty() } } - private fun buildServerResponse( - challenge: Ts43Challenge, - responseBody: String - ): ChallengeResponse { - return ChallengeResponse( - ts43_challenge_response = Ts43ChallengeResponse( - ts43_type = challenge.ts43_type, - server_challenge_response = ServerChallengeResponse( - acquire_temporary_token_response = responseBody - ), - http_history = httpHistory.toList() - ) - ) - } - - private fun buildClientResponse( - challenge: Ts43Challenge, - responseBody: String - ): ChallengeResponse { - return ChallengeResponse( - ts43_challenge_response = Ts43ChallengeResponse( - ts43_type = challenge.ts43_type, - client_challenge_response = ClientChallengeResponse( - get_phone_number_response = responseBody - ), - http_history = httpHistory.toList() - ) - ) - } - private fun buildFailureResponse( challenge: Ts43Challenge, statusCode: Ts43ChallengeResponseStatus.Code @@ -408,23 +369,44 @@ class Ts43Verifier(private val context: Context, private val subId: Int) { ) ) } +} - private fun buildApiErrorResponse( - challenge: Ts43Challenge, - error: Ts43ApiException - ): ChallengeResponse { - return ChallengeResponse( - ts43_challenge_response = Ts43ChallengeResponse( - ts43_type = challenge.ts43_type, - status = Ts43ChallengeResponseStatus( - error = Ts43ChallengeResponseError( - error_code = error.errorCode, - http_status = error.httpStatus, - request_type = error.requestType - ) - ), - 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 val okHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .followRedirects(false) + .followSslRedirects(false) + .cookieJar(object : CookieJar { + private val cookieStore = mutableMapOf>() + override fun saveFromResponse(url: HttpUrl, cookies: List) { + 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 { + return cookieStore[url.host] + ?.filter { cookie -> cookie.matches(url) } + .orEmpty() + } + }) + .build() } From 01400ff25b4a27b1f90905ada67ca04c1e86cda3 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:38:08 -0400 Subject: [PATCH 04/63] More refactoring --- .../gms/constellation/core/AuthManager.kt | 39 +++++++------- .../gms/constellation/core/GetIidToken.kt | 2 +- .../core/GetVerifiedPhoneNumbers.kt | 2 +- .../gms/constellation/core/RpcClient.kt | 54 +++++++------------ .../core/proto/builders/ClientInfoBuilder.kt | 4 +- .../core/proto/builders/CommonBuilders.kt | 3 +- .../proto/builders/RequestBuildContext.kt | 3 +- 7 files changed, 49 insertions(+), 58 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt index 94b4bdb8a8..371e677448 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt @@ -13,8 +13,11 @@ import java.security.Signature import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec -class AuthManager(context: Context) { +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" @@ -24,22 +27,14 @@ class AuthManager(context: Context) { @Volatile private var instance: AuthManager? = null - fun get(context: Context): AuthManager { - val existing = instance - if (existing != null) return existing - - return synchronized(this) { - instance ?: AuthManager(context).also { instance = it } - } + fun get(context: Context): AuthManager = instance ?: synchronized(this) { + instance ?: AuthManager(context).also { instance = it } } } - private val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - // GMS signing format: {iidToken}:{seconds}:{nanos} fun signIidToken(iidToken: String): Pair { - val now = System.currentTimeMillis() - val timestamp = Instant.ofEpochMilli(now) + val timestamp = Instant.ofEpochMilli(System.currentTimeMillis()) val content = "$iidToken:${timestamp.epochSecond}:${timestamp.nano}" return sign(content) to timestamp } @@ -61,10 +56,20 @@ class AuthManager(context: Context) { try { val kf = KeyFactory.getInstance("EC") val privateKey = kf.generatePrivate( - PKCS8EncodedKeySpec(Base64.decode(privateKeyStr, Base64.DEFAULT)) + PKCS8EncodedKeySpec( + Base64.decode( + privateKeyStr, + Base64.DEFAULT + ) + ) ) val publicKey = kf.generatePublic( - X509EncodedKeySpec(Base64.decode(publicKeyStr, Base64.DEFAULT)) + X509EncodedKeySpec( + Base64.decode( + publicKeyStr, + Base64.DEFAULT + ) + ) ) return KeyPair(publicKey, privateKey) } catch (_: Exception) { @@ -96,7 +101,5 @@ class AuthManager(context: Context) { } } - fun getFid(): String { - return InstanceID.getInstance(context).id - } -} + fun getFid(): String = InstanceID.getInstance(context).id +} \ No newline at end of file diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt index 9ba0e0b8af..d7dc64cffe 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt @@ -18,7 +18,7 @@ suspend fun handleGetIidToken( request: GetIidTokenRequest ) = withContext(Dispatchers.IO) { try { - val authManager = AuthManager.get(context) + val authManager = context.authManager val iidToken = authManager.getIidToken(request.projectNumber?.toString()) val fid = authManager.getFid() val (signature, timestamp) = authManager.signIidToken(iidToken) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index 5ab87e178c..d311bcf562 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -41,7 +41,7 @@ internal suspend fun fetchVerifiedPhoneNumbers( bundle: Bundle, callingPackage: String = bundle.getString("calling_package") ?: context.packageName ): List = withContext(Dispatchers.IO) { - val authManager = AuthManager.get(context) + val authManager = context.authManager val sessionId = UUID.randomUUID().toString() val selections = extractPhoneNumberSelections(bundle) val certificateHash = bundle.getString("certificate_hash") ?: "" diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt index 57495656c3..e5030bac66 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt @@ -1,47 +1,33 @@ package org.microg.gms.constellation.core import com.squareup.wire.GrpcClient -import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.Response import org.microg.gms.common.Constants import org.microg.gms.constellation.core.proto.PhoneDeviceVerificationClient import org.microg.gms.constellation.core.proto.PhoneNumberClient -private class AuthInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - 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()) - - return chain.proceed(builder.build()) - } -} - object RpcClient { - private val client: OkHttpClient by lazy { - OkHttpClient.Builder() - .addInterceptor(AuthInterceptor()) - .build() - } - - private val grpcClient: GrpcClient by lazy { - 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 by lazy { + private val client: OkHttpClient = OkHttpClient.Builder() + .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::class) - } - val phoneNumberClient: PhoneNumberClient by lazy { + val phoneNumberClient: PhoneNumberClient = grpcClient.create(PhoneNumberClient::class) - } } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt index bc97172d63..8e61d1005b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt @@ -14,6 +14,7 @@ import org.microg.gms.common.Constants import org.microg.gms.constellation.core.AuthManager 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 @@ -47,11 +48,10 @@ suspend operator fun ClientInfo.Companion.invoke( ): ClientInfo { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val locale = Locale.getDefault().let { "${it.language}_${it.country}" } - val authManager = AuthManager.get(context) return ClientInfo( device_id = DeviceID(context, buildContext.iidToken), - client_public_key = authManager.getOrCreateKeyPair().public.encoded.toByteString(), + client_public_key = context.authManager.getOrCreateKeyPair().public.encoded.toByteString(), locale = locale, gmscore_version_number = Constants.GMS_VERSION_CODE / 1000, gmscore_version = packageInfo.versionName ?: "", diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt index d2f6f1e0da..c79cdbff62 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import okio.ByteString.Companion.toByteString import org.microg.gms.constellation.core.AuthManager +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 @@ -46,7 +47,7 @@ suspend operator fun RequestHeader.Companion.invoke( triggerType: RequestTrigger.Type = RequestTrigger.Type.CONSENT_API_TRIGGER, includeClientAuth: Boolean = false ): RequestHeader { - val authManager = if (includeClientAuth) AuthManager.get(context) else null + 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( diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt index c581237c17..1262045264 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt @@ -2,6 +2,7 @@ package org.microg.gms.constellation.core.proto.builders 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( @@ -11,7 +12,7 @@ data class RequestBuildContext( suspend fun buildRequestContext( context: Context, - authManager: AuthManager = AuthManager.get(context) + authManager: AuthManager = context.authManager ): RequestBuildContext { return RequestBuildContext( iidToken = authManager.getIidToken(), From 3f15e8bf4011898dce4b33deabecaba58b4349ea Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 03:40:55 -0400 Subject: [PATCH 05/63] Add `android.permission.USE_CREDENTIALS` into `AndroidManifest.xml` --- play-services-constellation/core/src/main/AndroidManifest.xml | 1 + .../gms/constellation/core/proto/builders/GaiaInfoBuilder.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml index cbe65637d0..bd95282658 100644 --- a/play-services-constellation/core/src/main/AndroidManifest.xml +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt index febce16e53..31ee9146ab 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt @@ -23,6 +23,7 @@ operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? { val md = MessageDigest.getInstance("SHA-256") for (account in accounts) { + // Note: Simple implementation, maybe do actual obfuscated Gaia ID retrieval later? val hash = md.digest(account.name.toByteArray(Charsets.UTF_8)) val number = hash.take(8).fold(0L) { acc, byte -> (acc shl 8) or (byte.toLong() and 0xFF) } From 65961f8491cbe0e8c7c898d39da33742f3cbec9b Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:15:36 -0400 Subject: [PATCH 06/63] Fix `NewApi` lints --- .../gms/constellation/core/AuthManager.kt | 21 ++++++- .../core/ConstellationApiService.kt | 5 ++ .../core/ConstellationStateStore.kt | 4 ++ .../gms/constellation/core/GetIidToken.kt | 4 +- .../core/GetVerifiedPhoneNumbers.kt | 17 ++++-- .../core/VerificationMappings.kt | 3 + .../constellation/core/VerifyPhoneNumber.kt | 4 +- .../core/proto/builders/ClientInfoBuilder.kt | 8 ++- .../core/proto/builders/CommonBuilders.kt | 5 +- .../core/proto/builders/GaiaInfoBuilder.kt | 5 ++ .../core/proto/builders/SyncRequestBuilder.kt | 10 +--- .../proto/builders/TelephonyInfoBuilder.kt | 7 ++- .../core/verification/CarrierIdVerifier.kt | 3 + .../core/verification/ChallengeProcessor.kt | 4 ++ .../core/verification/MoSmsVerifier.kt | 55 ++++++++++++------- .../verification/RegisteredSmsVerifier.kt | 8 ++- .../core/verification/Ts43Verifier.kt | 7 ++- .../ts43/ServiceEntitlementExtension.kt | 6 ++ 18 files changed, 127 insertions(+), 49 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt index 371e677448..ba601c3832 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt @@ -1,7 +1,10 @@ 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 @@ -24,6 +27,8 @@ class AuthManager private constructor(context: Context) { 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 @@ -33,10 +38,20 @@ class AuthManager private constructor(context: Context) { } // GMS signing format: {iidToken}:{seconds}:{nanos} + fun signIidTokenCompat(iidToken: String): Pair { + 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 { - val timestamp = Instant.ofEpochMilli(System.currentTimeMillis()) - val content = "$iidToken:${timestamp.epochSecond}:${timestamp.nano}" - return sign(content) to timestamp + val (bytes, millis) = signIidTokenCompat(iidToken) + return bytes to Instant.ofEpochMilli(millis) } fun getIidToken(projectNumber: String? = null): String { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt index f4ddbb4123..39334ec569 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt @@ -1,6 +1,7 @@ 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 @@ -76,6 +77,7 @@ class ConstellationApiServiceImpl( }" ) if (cb == null || bundle == null) return + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return serviceScope.launch { handleVerifyPhoneNumberV1(context, cb, bundle, packageName) } } @@ -86,6 +88,7 @@ class ConstellationApiServiceImpl( ) { Log.i(TAG, "verifyPhoneNumberSingleUse()") if (cb == null || bundle == null) return + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return serviceScope.launch { handleVerifyPhoneNumberSingleUse(context, cb, bundle, packageName) } } @@ -99,6 +102,7 @@ class ConstellationApiServiceImpl( "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, cb, request, packageName) } } @@ -119,6 +123,7 @@ class ConstellationApiServiceImpl( ) { 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, cb, request) } } } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt index dc802dfacc..f41739c10b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt @@ -1,8 +1,12 @@ +@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 diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt index d7dc64cffe..5ee8a6ac71 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt @@ -21,11 +21,11 @@ suspend fun handleGetIidToken( val authManager = context.authManager val iidToken = authManager.getIidToken(request.projectNumber?.toString()) val fid = authManager.getFid() - val (signature, timestamp) = authManager.signIidToken(iidToken) + val (signature, timestamp) = authManager.signIidTokenCompat(iidToken) callbacks.onIidTokenGenerated( Status.SUCCESS, - GetIidTokenResponse(iidToken, fid, signature, timestamp.toEpochMilli()), + GetIidTokenResponse(iidToken, fid, signature, timestamp), ApiMetadata.DEFAULT ) } catch (e: Exception) { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index d311bcf562..e02b3565fa 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -1,6 +1,9 @@ +@file:SuppressLint("NewApi") package org.microg.gms.constellation.core +import android.annotation.SuppressLint import android.content.Context +import android.os.Build import android.os.Bundle import android.util.Log import com.google.android.gms.common.api.ApiMetadata @@ -17,7 +20,7 @@ import org.microg.gms.constellation.core.proto.GetVerifiedPhoneNumbersRequest.Ph import org.microg.gms.constellation.core.proto.IIDTokenAuth import org.microg.gms.constellation.core.proto.TokenOption import java.util.UUID -import org.microg.gms.constellation.core.proto.VerifiedPhoneNumber as RpcVerifiedPhoneNumber +import org.microg.gms.constellation.core.proto.VerifiedPhoneNumber private const val TAG = "GetVerifiedPhoneNumbers" @@ -27,6 +30,10 @@ suspend fun handleGetVerifiedPhoneNumbers( bundle: Bundle ) = withContext(Dispatchers.IO) { try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + throw Exception("Unsupported SDK") + } + val phoneNumbers = fetchVerifiedPhoneNumbers(context, bundle).map { it.toPhoneNumberInfo() } callbacks.onPhoneNumberVerified(Status.SUCCESS, phoneNumbers, ApiMetadata.DEFAULT) @@ -40,7 +47,7 @@ internal suspend fun fetchVerifiedPhoneNumbers( context: Context, bundle: Bundle, callingPackage: String = bundle.getString("calling_package") ?: context.packageName -): List = withContext(Dispatchers.IO) { +): List = withContext(Dispatchers.IO) { val authManager = context.authManager val sessionId = UUID.randomUUID().toString() val selections = extractPhoneNumberSelections(bundle) @@ -78,14 +85,14 @@ internal suspend fun fetchVerifiedPhoneNumbers( response.phone_numbers } -internal fun List.toVerifyPhoneNumberResponse(): VerifyPhoneNumberResponse { +internal fun List.toVerifyPhoneNumberResponse(): VerifyPhoneNumberResponse { return VerifyPhoneNumberResponse( map { it.toPhoneNumberVerification() }.toTypedArray(), Bundle.EMPTY ) } -private fun RpcVerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo { +private fun VerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo { val extras = Bundle().apply { if (id_token.isNotEmpty()) { putString("id_token", id_token) @@ -101,7 +108,7 @@ private fun RpcVerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo { ) } -private fun RpcVerifiedPhoneNumber.toPhoneNumberVerification(): PhoneNumberVerification { +private fun VerifiedPhoneNumber.toPhoneNumberVerification(): PhoneNumberVerification { val extras = Bundle().apply { putInt("rcs_state", rcs_state.value) } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt index 62255e1fc0..6102ca047a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt @@ -1,6 +1,8 @@ package org.microg.gms.constellation.core +import android.os.Build import android.os.Bundle +import androidx.annotation.RequiresApi import com.google.android.gms.constellation.PhoneNumberVerification import org.microg.gms.constellation.core.proto.Param import org.microg.gms.constellation.core.proto.UnverifiedInfo @@ -52,6 +54,7 @@ fun Verification.getVerificationStatus(): Verification.Status { } } +@RequiresApi(Build.VERSION_CODES.O) fun Verification.toClientVerification(imsiToSlotMap: Map): PhoneNumberVerification { val verificationStatus = this.getVerificationStatus() var phoneNumber: String? = null diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 1d07ea48c3..e456fb00c4 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -1,3 +1,5 @@ +@file:RequiresApi(Build.VERSION_CODES.O) + package org.microg.gms.constellation.core import android.content.Context @@ -234,7 +236,6 @@ private suspend fun handleVerifyPhoneNumberRequest( } } -@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) private suspend fun runVerificationFlow( context: Context, request: VerifyPhoneNumberRequest, @@ -292,7 +293,6 @@ private fun PhoneNumberVerification.toLegacyPhoneNumberInfoOrNull(): PhoneNumber return PhoneNumberInfo(1, phoneNumber, timestampMillis, extras) } -@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) private suspend fun executeSyncFlow( context: Context, sessionId: String, diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt index 8e61d1005b..8a67cb84ad 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt @@ -1,3 +1,5 @@ +@file:RequiresApi(Build.VERSION_CODES.O) + package org.microg.gms.constellation.core.proto.builders import android.annotation.SuppressLint @@ -6,12 +8,12 @@ import android.os.Build import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import android.util.Log +import androidx.annotation.RequiresApi import androidx.core.content.edit import androidx.core.content.getSystemService import com.google.android.gms.tasks.await import okio.ByteString.Companion.toByteString import org.microg.gms.common.Constants -import org.microg.gms.constellation.core.AuthManager import org.microg.gms.constellation.core.ConstellationStateStore import org.microg.gms.constellation.core.GServices import org.microg.gms.constellation.core.authManager @@ -71,6 +73,7 @@ suspend operator fun ClientInfo.Companion.invoke( ) } +@SuppressLint("HardwareIds") operator fun DeviceID.Companion.invoke(context: Context, iidToken: String): DeviceID { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -123,9 +126,8 @@ operator fun CountryInfo.Companion.invoke(context: Context): CountryInfo { val tm = context.getSystemService() sm?.activeSubscriptionInfoList?.forEach { info -> - val targetTM = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val targetTM = tm?.createForSubscriptionId(info.subscriptionId) - } else tm targetTM?.networkCountryIso?.let { networkCountries.add(it.lowercase()) } info.countryIso?.let { simCountries.add(it.lowercase()) } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt index c79cdbff62..ae7e49ba83 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt @@ -1,9 +1,11 @@ + package org.microg.gms.constellation.core.proto.builders 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.authManager import org.microg.gms.constellation.core.proto.AuditToken import org.microg.gms.constellation.core.proto.AuditTokenMetadata @@ -40,6 +42,7 @@ fun AuditToken.Companion.generate(): AuditToken { ) } +@RequiresApi(Build.VERSION_CODES.O) suspend operator fun RequestHeader.Companion.invoke( context: Context, sessionId: String, diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt index 31ee9146ab..60ff924562 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt @@ -1,8 +1,11 @@ + package org.microg.gms.constellation.core.proto.builders 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 @@ -16,6 +19,8 @@ import kotlin.math.absoluteValue private const val TAG = "GaiaInfoBuilder" + +@RequiresApi(Build.VERSION_CODES.O) operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? { val entries = mutableListOf() try { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt index 61bc46d639..e2553dcb9b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt @@ -1,3 +1,6 @@ +@file:RequiresApi(Build.VERSION_CODES.O) +@file:SuppressLint("HardwareIds") + package org.microg.gms.constellation.core.proto.builders import android.annotation.SuppressLint @@ -30,8 +33,6 @@ import org.microg.gms.constellation.core.proto.VerificationPolicy private const val TAG = "SyncRequestBuilder" -@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) -@SuppressLint("HardwareIds", "MissingPermission") fun buildImsiToSubscriptionInfoMap(context: Context): Map { val subscriptionManager = context.getSystemService() ?: return emptyMap() @@ -56,7 +57,6 @@ fun buildImsiToSubscriptionInfoMap(context: Context): Map() val targetTm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && subscriptionId >= 0) { @@ -107,7 +111,6 @@ fun NetworkSignal.Companion.getList(context: Context): List { return connectivityInfos } -@SuppressLint("HardwareIds") fun SimOperatorInfo.Companion.getList(context: Context): List { val infos = mutableListOf() try { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt index 53c1bcc72d..00d0fb8919 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt @@ -1,9 +1,12 @@ +@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 diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index ac79655766..f01997d252 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -1,8 +1,12 @@ +@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 org.microg.gms.constellation.core.ConstellationStateStore diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index 19fd2d0305..5ff49b42c9 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -1,3 +1,5 @@ +@file:RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) + package org.microg.gms.constellation.core.verification import android.Manifest @@ -11,6 +13,8 @@ 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.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull @@ -21,7 +25,7 @@ import java.util.UUID import kotlin.coroutines.resume private const val TAG = "MoSmsVerifier" -private const val ACTION_MO_SMS_SENT = "org.microg.gms.constellation.coreMO_SMS_SENT" +private const val ACTION_MO_SMS_SENT = "org.microg.gms.constellation.core.MO_SMS_SENT" suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse { if (proxy_number.isEmpty() || sms.isEmpty()) { @@ -105,7 +109,12 @@ suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse Context.RECEIVER_NOT_EXPORTED ) } else { - context.registerReceiver(receiver, IntentFilter(action)) + ContextCompat.registerReceiver( + context, + receiver, + IntentFilter(action), + ContextCompat.RECEIVER_NOT_EXPORTED + ) } continuation.invokeOnCancellation { @@ -158,37 +167,41 @@ suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse ) } -private fun isActiveSubscription(context: Context, subscriptionId: Int): Boolean { - if (subscriptionId == -1) return false +private fun isActiveSubscription(context: Context, subId: Int): Boolean { + if (subId == -1) return true + return try { - context.getSystemService()?.isActiveSubscriptionId(subscriptionId) - ?: false + val subManager = context.getSystemService() ?: 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 $subscriptionId", e) + Log.w(TAG, "Failed to query active subscription for $subId", e) false } } +@Suppress("DEPRECATION") private fun resolveSmsManager(context: Context, subId: Int): SmsManager? { if (subId != -1 && !isActiveSubscription(context, subId)) { return null } + return try { - val manager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - context.getSystemService(SmsManager::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val manager = context.getSystemService(SmsManager::class.java) + if (subId != -1) { + manager?.createForSubscriptionId(subId) + } else { + manager + } } else { - null - } - when { - subId != -1 && manager != null -> manager.createForSubscriptionId(subId) - manager != null -> manager - subId != -1 -> { - @Suppress("DEPRECATION") + if (subId != -1) { SmsManager.getSmsManagerForSubscriptionId(subId) - } - - else -> { - @Suppress("DEPRECATION") + } else { SmsManager.getDefault() } } @@ -196,4 +209,4 @@ private fun resolveSmsManager(context: Context, subId: Int): SmsManager? { Log.e(TAG, "Failed to resolve SmsManager for subId: $subId", e) null } -} +} \ No newline at end of file diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt index ceb4395bb1..14e4e42f87 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt @@ -1,10 +1,14 @@ +@file:RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) + package org.microg.gms.constellation.core.verification import android.content.Context +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.getSystemService import okio.ByteString.Companion.toByteString import org.microg.gms.constellation.core.VerificationSettingsPhenotypes @@ -108,7 +112,7 @@ private fun getLocalNumbers(context: Context, targetSubId: Int): List { try { subscriptionManager?.activeSubscriptionInfoList.orEmpty().forEach { info -> if (targetSubId != -1 && info.subscriptionId != targetSubId) return@forEach - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU && subscriptionManager != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && subscriptionManager != null) { numbers += subscriptionManager.getPhoneNumber( info.subscriptionId, SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER @@ -123,7 +127,7 @@ private fun getLocalNumbers(context: Context, targetSubId: Int): List { ) } val targetManager = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { telephonyManager?.createForSubscriptionId(info.subscriptionId) } else { telephonyManager diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt index ebaa6733ef..a43ef69f95 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt @@ -1,9 +1,14 @@ +@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 @@ -46,7 +51,7 @@ private class InternalTs43Verifier(private val context: Context, private val sub val tm = requireNotNull(context.getSystemService()) { "TelephonyManager unavailable" } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && subId >= 0) { + if (subId >= 0) { tm.createForSubscriptionId(subId) } else tm } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt index 69e7e846cb..6fe7256578 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt @@ -1,7 +1,13 @@ +@file:RequiresApi(Build.VERSION_CODES.O) +@file:SuppressLint("HardwareIds") + 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 From 52a58d4e9b54232b0e72e295a0f24b399f7ee507 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:55:23 -0400 Subject: [PATCH 07/63] Fix the rest of the lints --- .../core/proto/builders/SyncRequestBuilder.kt | 11 ++++++---- .../proto/builders/TelephonyInfoBuilder.kt | 1 + .../core/verification/MoSmsVerifier.kt | 5 +++++ .../verification/RegisteredSmsVerifier.kt | 21 ++++++++++++++++--- .../ts43/ServiceEntitlementExtension.kt | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt index e2553dcb9b..2be034dba2 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt @@ -3,14 +3,17 @@ package org.microg.gms.constellation.core.proto.builders +import android.Manifest import android.annotation.SuppressLint import android.content.Context +import android.content.pm.PackageManager import android.os.Build 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 @@ -42,11 +45,8 @@ fun buildImsiToSubscriptionInfoMap(context: Context): Map - val subsTelephonyManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val subsTelephonyManager = telephonyManager.createForSubscriptionId(info.subscriptionId) - } else { - telephonyManager - } subsTelephonyManager.subscriberId?.let { imsi -> map[imsi] = info } @@ -63,6 +63,9 @@ fun getTelephonyPhoneNumbers( ): List { 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() ?: return emptyList() diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt index fabda054a5..a66a562957 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt @@ -22,6 +22,7 @@ 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() val targetTm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && subscriptionId >= 0) { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index 5ff49b42c9..c1f746d109 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -170,6 +170,11 @@ suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse 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() ?: return false diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt index 14e4e42f87..aa8f8044b8 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt @@ -2,13 +2,17 @@ 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 @@ -90,9 +94,6 @@ fun RegisteredSmsChallenge.verify(context: Context, subId: Int): ChallengeRespon } } } - } catch (e: SecurityException) { - Log.w(TAG, "SMS inbox access denied", e) - return ChallengeResponse(registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList())) } catch (e: Exception) { Log.e(TAG, "Registered SMS verification failed", e) return ChallengeResponse(registered_sms_response = RegisteredSmsChallengeResponse(items = emptyList())) @@ -103,12 +104,26 @@ fun RegisteredSmsChallenge.verify(context: Context, subId: Int): ChallengeRespon ) } +@SuppressLint("HardwareIds") private fun getLocalNumbers(context: Context, targetSubId: Int): List { val numbers = linkedSetOf() val subscriptionManager = context.getSystemService() val telephonyManager = context.getSystemService() + val hasState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED + val isCarrier = telephonyManager?.hasCarrierPrivileges() == true + 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 (!isCarrier && (!hasState || !hasNumbers)) { + Log.e(TAG, "Permission not granted") + return emptyList() + } + try { subscriptionManager?.activeSubscriptionInfoList.orEmpty().forEach { info -> if (targetSubId != -1 && info.subscriptionId != targetSubId) return@forEach diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt index 6fe7256578..56d8114022 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt @@ -1,5 +1,5 @@ @file:RequiresApi(Build.VERSION_CODES.O) -@file:SuppressLint("HardwareIds") +@file:SuppressLint("HardwareIds", "MissingPermission") package org.microg.gms.constellation.core.verification.ts43 From 3917eeba97fa5bae9ec0ea164044e97f4bb463b3 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:20:33 -0400 Subject: [PATCH 08/63] Convert parcelables into Java --- .../gms/constellation/GetPnvCapabilities.kt | 9 +- .../constellation/core/GetPnvCapabilities.kt | 7 +- .../core/GetVerifiedPhoneNumbers.kt | 2 +- .../core/VerificationMappings.kt | 2 +- .../constellation/core/VerifyPhoneNumber.kt | 52 ++++---- .../gms/constellation/GetIidTokenRequest.java | 32 +++++ .../constellation/GetIidTokenResponse.java | 47 +++++++ .../GetPnvCapabilitiesRequest.java | 42 ++++++ .../GetPnvCapabilitiesResponse.java | 101 +++++++++++++++ .../gms/constellation/PhoneNumberInfo.java | 49 +++++++ .../VerifyPhoneNumberRequest.java | 120 ++++++++++++++++++ .../VerifyPhoneNumberResponse.java | 96 ++++++++++++++ .../android/gms/constellation/GetIidToken.kt | 43 ------- .../gms/constellation/GetPnvCapabilities.kt | 63 --------- .../gms/constellation/PhoneNumberInfo.kt | 27 ---- .../gms/constellation/VerifyPhoneNumber.kt | 91 ------------- 16 files changed, 520 insertions(+), 263 deletions(-) create mode 100644 play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java create mode 100644 play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java create mode 100644 play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java create mode 100644 play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java create mode 100644 play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java create mode 100644 play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java create mode 100644 play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java delete mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt delete mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt delete mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt delete mode 100644 play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt diff --git a/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt b/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt index 3bd89b61e0..79565ca454 100644 --- a/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt +++ b/play-services-constellation/core/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt @@ -1,5 +1,7 @@ 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), @@ -16,14 +18,11 @@ enum class VerificationStatus(val value: Int) { } } -operator fun VerificationCapability.Companion.invoke( +fun verificationCapability( verificationMethod: Int, status: VerificationStatus ): VerificationCapability { - return VerificationCapability( - verificationMethod = verificationMethod, - statusValue = status.value - ) + return VerificationCapability(verificationMethod, status.value) } val VerificationCapability.status: VerificationStatus diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilities.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilities.kt index 72d4617f2e..5aeacfcf12 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilities.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetPnvCapabilities.kt @@ -13,11 +13,10 @@ 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.SimCapability -import com.google.android.gms.constellation.VerificationCapability +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.invoke +import com.google.android.gms.constellation.verificationCapability import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.security.MessageDigest @@ -57,7 +56,7 @@ suspend fun handleGetPnvCapabilities( // GMS hardcodes public verification method 9 for the Firebase PNV TS43 capability path. val verificationCapabilities = if (9 in request.verificationTypes) { listOf( - VerificationCapability( + verificationCapability( 9, when { !GetPnvCapabilitiesApiPhenotype.FPNV_ALLOWED_CARRIER_IDS.contains( diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index e02b3565fa..b344d37d86 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -9,7 +9,7 @@ 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.PhoneNumberInfo -import com.google.android.gms.constellation.PhoneNumberVerification +import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification import com.google.android.gms.constellation.VerifyPhoneNumberResponse import com.google.android.gms.constellation.internal.IConstellationCallbacks import kotlinx.coroutines.Dispatchers diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt index 6102ca047a..2fae9c903b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt @@ -3,7 +3,7 @@ package org.microg.gms.constellation.core import android.os.Build import android.os.Bundle import androidx.annotation.RequiresApi -import com.google.android.gms.constellation.PhoneNumberVerification +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 diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index e456fb00c4..32c5bd82d8 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -10,9 +10,9 @@ 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.IdTokenRequest +import com.google.android.gms.constellation.VerifyPhoneNumberRequest.IdTokenRequest import com.google.android.gms.constellation.PhoneNumberInfo -import com.google.android.gms.constellation.PhoneNumberVerification +import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification import com.google.android.gms.constellation.VerifyPhoneNumberRequest import com.google.android.gms.constellation.VerifyPhoneNumberResponse import com.google.android.gms.constellation.internal.IConstellationCallbacks @@ -54,17 +54,17 @@ suspend fun handleVerifyPhoneNumberV1( else -> 300L } val request = VerifyPhoneNumberRequest( - policyId = extras.getString("policy_id", ""), - timeout = timeout, - idTokenRequest = IdTokenRequest( + extras.getString("policy_id", ""), + timeout, + IdTokenRequest( extras.getString("certificate_hash", ""), extras.getString("token_nonce", "") ), - extras = extras, - targetedSims = emptyList(), - silent = false, - apiVersion = 2, - verificationMethodsValues = emptyList() + extras, + emptyList(), + false, + 2, + emptyList() ) val policyId = bundle.getString("policy_id", "") val mode = bundle.getInt("verification_mode", 0) @@ -103,17 +103,17 @@ suspend fun handleVerifyPhoneNumberSingleUse( else -> 300L } val request = VerifyPhoneNumberRequest( - policyId = extras.getString("policy_id", ""), - timeout = timeout, - idTokenRequest = IdTokenRequest( + extras.getString("policy_id", ""), + timeout, + IdTokenRequest( extras.getString("certificate_hash", ""), extras.getString("token_nonce", "") ), - extras = extras, - targetedSims = emptyList(), - silent = false, - apiVersion = 2, - verificationMethodsValues = emptyList() + extras, + emptyList(), + false, + 2, + emptyList() ) handleVerifyPhoneNumberRequest( @@ -133,25 +133,21 @@ suspend fun handleVerifyPhoneNumberRequest( packageName: String? ) { val callingPackage = packageName ?: context.packageName - val requestWithExtras = request.copy( - extras = Bundle(request.extras).apply { - putString("calling_api", "verifyPhoneNumber") - } - ) - val useReadPath = when (requestWithExtras.apiVersion) { - 0 -> requestWithExtras.policyId in VerifyPhoneNumberApiPhenotypes.READ_ONLY_POLICY_IDS + 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 -> requestWithExtras.policyId in VerifyPhoneNumberApiPhenotypes.POLICY_IDS_ALLOWED_FOR_LOCAL_READ + 3 -> request.policyId in VerifyPhoneNumberApiPhenotypes.POLICY_IDS_ALLOWED_FOR_LOCAL_READ else -> false } handleVerifyPhoneNumberRequest( context, callbacks, - requestWithExtras, + request, callingPackage, if (useReadPath) ReadCallbackMode.TYPED else ReadCallbackMode.NONE, - localReadFallback = requestWithExtras.apiVersion == 3 && useReadPath, + localReadFallback = request.apiVersion == 3 && useReadPath, legacyCallbackOnFullFlow = false ) } diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java new file mode 100644 index 0000000000..5467842558 --- /dev/null +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java @@ -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 { + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(GetIidTokenRequest.class); +} \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java new file mode 100644 index 0000000000..0c58b7238b --- /dev/null +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java @@ -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 { + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(GetIidTokenResponse.class); +} \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java new file mode 100644 index 0000000000..aeafd2b357 --- /dev/null +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java @@ -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 { + @Field(1) + public final String policyId; + + @Field(2) + public final List verificationTypes; + + @Field(3) + public final List simSlotIndices; + + @Constructor + public GetPnvCapabilitiesRequest( + @Param(1) String policyId, + @Param(2) List verificationTypes, + @Param(3) List simSlotIndices + ) { + this.policyId = policyId; + this.verificationTypes = verificationTypes; + this.simSlotIndices = simSlotIndices; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(GetPnvCapabilitiesRequest.class); +} \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java new file mode 100644 index 0000000000..e5dea8c34c --- /dev/null +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java @@ -0,0 +1,101 @@ +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 { + @Field(1) + public final List simCapabilities; + + @Constructor + public GetPnvCapabilitiesResponse( + @Param(1) List simCapabilities + ) { + this.simCapabilities = simCapabilities; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(GetPnvCapabilitiesResponse.class); + + @SafeParcelable.Class + public static class SimCapability extends AbstractSafeParcelable { + + @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 verificationCapabilities; + + @Constructor + public SimCapability( + @Param(1) int slotValue, + @Param(2) String subscriberIdDigest, + @Param(3) int carrierId, + @Param(4) String operatorName, + @Param(5) List 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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(SimCapability.class); + } + + @SafeParcelable.Class + public static class VerificationCapability extends AbstractSafeParcelable { + + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(VerificationCapability.class); + } +} \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java new file mode 100644 index 0000000000..82b4ce0268 --- /dev/null +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java @@ -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 { + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(PhoneNumberInfo.class); +} \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java new file mode 100644 index 0000000000..e554934ae6 --- /dev/null +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java @@ -0,0 +1,120 @@ +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 { + @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 targetedSims; + + @Field(6) + public final boolean silent; + + @Field(7) + public final int apiVersion; + + @Field(8) + public final List verificationMethodsValues; + + @Constructor + public VerifyPhoneNumberRequest( + @Param(1) String policyId, + @Param(2) long timeout, + @Param(3) IdTokenRequest idTokenRequest, + @Param(4) Bundle extras, + @Param(5) List targetedSims, + @Param(6) boolean silent, + @Param(7) int apiVersion, + @Param(8) List verificationMethodsValues + ) { + this.policyId = policyId; + this.timeout = timeout; + this.idTokenRequest = idTokenRequest; + this.extras = extras; + this.targetedSims = targetedSims; + this.silent = silent; + this.apiVersion = apiVersion; + this.verificationMethodsValues = verificationMethodsValues; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(VerifyPhoneNumberRequest.class); + + @SafeParcelable.Class + public static class IdTokenRequest extends AbstractSafeParcelable { + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(IdTokenRequest.class); + } + + @SafeParcelable.Class + public static class ImsiRequest extends AbstractSafeParcelable { + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(ImsiRequest.class); + } +} \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java new file mode 100644 index 0000000000..4a94badd74 --- /dev/null +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java @@ -0,0 +1,96 @@ +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 { + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(VerifyPhoneNumberResponse.class); + + @SafeParcelable.Class + public static class PhoneNumberVerification extends AbstractSafeParcelable { + @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); + } + + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(PhoneNumberVerification.class); + } +} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt deleted file mode 100644 index 53760184ab..0000000000 --- a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetIidToken.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.google.android.gms.constellation - -import android.os.Parcel -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.SafeParcelable.Constructor -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param -import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter - -@SafeParcelable.Class -data class GetIidTokenRequest @Constructor constructor( - @JvmField @Param(1) @Field(1) val projectNumber: Long? -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) { - CREATOR.writeToParcel(this, out, flags) - } - - companion object { - @JvmField - val CREATOR: SafeParcelableCreatorAndWriter = - findCreator(GetIidTokenRequest::class.java) - } -} - -@SafeParcelable.Class -data class GetIidTokenResponse @Constructor constructor( - @JvmField @Param(1) @Field(1) val iidToken: String, - @JvmField @Param(2) @Field(2) val fid: String, - @JvmField @Param(3) @Field(value = 3, type = "byte[]") val signature: ByteArray?, - @JvmField @Param(4) @Field(4) val timestamp: Long -) : AbstractSafeParcelable() { - - override fun writeToParcel(out: Parcel, flags: Int) { - CREATOR.writeToParcel(this, out, flags) - } - - companion object { - @JvmField - val CREATOR: SafeParcelableCreatorAndWriter = - findCreator(GetIidTokenResponse::class.java) - } -} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt deleted file mode 100644 index fe80003950..0000000000 --- a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/GetPnvCapabilities.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.google.android.gms.constellation - -import android.os.Parcel -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.SafeParcelable.Constructor -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param - -@SafeParcelable.Class -data class GetPnvCapabilitiesRequest @Constructor constructor( - @JvmField @Param(1) @Field(1) val policyId: String, - @JvmField @Param(2) @Field(2) val verificationTypes: List, - @JvmField @Param(3) @Field(3) val simSlotIndices: List -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(GetPnvCapabilitiesRequest::class.java) - } -} - -@SafeParcelable.Class -data class GetPnvCapabilitiesResponse @Constructor constructor( - @JvmField @Param(1) @Field(1) val simCapabilities: List -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(GetPnvCapabilitiesResponse::class.java) - } -} - -@SafeParcelable.Class -data class SimCapability @Constructor constructor( - @JvmField @Param(1) @Field(1) val slotValue: Int, - @JvmField @Param(2) @Field(2) val subscriberIdDigest: String, - @JvmField @Param(3) @Field(3) val carrierId: Int, - @JvmField @Param(4) @Field(4) val operatorName: String, - @JvmField @Param(5) @Field(5) val verificationCapabilities: List -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(SimCapability::class.java) - } -} - -@SafeParcelable.Class -data class VerificationCapability @Constructor constructor( - @JvmField @Param(1) @Field(1) val verificationMethod: Int, - @JvmField @Param(2) @Field(2) val statusValue: Int -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(VerificationCapability::class.java) - } -} \ No newline at end of file diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt deleted file mode 100644 index f16110f7f9..0000000000 --- a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/PhoneNumberInfo.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.google.android.gms.constellation - -import android.os.Bundle -import android.os.Parcel -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.SafeParcelable.Constructor -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param - -@SafeParcelable.Class -data class PhoneNumberInfo @Constructor constructor( - @JvmField @Param(1) @Field(1) val version: Int, - @JvmField @Param(2) @Field(2) val phoneNumber: String?, - @JvmField @Param(3) @Field(3) val verificationTime: Long, - @JvmField @Param(4) @Field(4) val extras: Bundle? -) : AbstractSafeParcelable() { - - override fun writeToParcel(out: Parcel, flags: Int) { - CREATOR.writeToParcel(this, out, flags) - } - - companion object { - @JvmField - val CREATOR = findCreator(PhoneNumberInfo::class.java) - } -} diff --git a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt b/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt deleted file mode 100644 index c2a0ef8115..0000000000 --- a/play-services-constellation/src/main/kotlin/com/google/android/gms/constellation/VerifyPhoneNumber.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.google.android.gms.constellation - -import android.os.Bundle -import android.os.Parcel -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.SafeParcelable.Constructor -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param -import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter - -@SafeParcelable.Class -data class VerifyPhoneNumberRequest @Constructor constructor( - @JvmField @Param(1) @Field(1) val policyId: String, - @JvmField @Param(2) @Field(2) val timeout: Long, - @JvmField @Param(3) @Field(3) val idTokenRequest: IdTokenRequest, - @JvmField @Param(4) @Field(4) val extras: Bundle, - @JvmField @Param(5) @Field(5) val targetedSims: List, - @JvmField @Param(6) @Field(6) val silent: Boolean, - @JvmField @Param(7) @Field(7) val apiVersion: Int, - @JvmField @Param(8) @Field(8) val verificationMethodsValues: List -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(VerifyPhoneNumberRequest::class.java) - } -} - -@SafeParcelable.Class -data class IdTokenRequest @Constructor constructor( - @JvmField @Param(1) @Field(1) val idToken: String, - @JvmField @Param(2) @Field(2) val subscriberHash: String -) : AbstractSafeParcelable() { - - override fun writeToParcel(out: Parcel, flags: Int) { - CREATOR.writeToParcel(this, out, flags) - } - - companion object { - @JvmField - val CREATOR: SafeParcelableCreatorAndWriter = - findCreator(IdTokenRequest::class.java) - } -} - -@SafeParcelable.Class -data class ImsiRequest @Constructor constructor( - @JvmField @Param(1) @Field(1) val imsi: String, - @JvmField @Param(2) @Field(2) val phoneNumberHint: String -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(ImsiRequest::class.java) - } -} - -@SafeParcelable.Class -data class VerifyPhoneNumberResponse @Constructor constructor( - @JvmField @Param(1) @Field(1) val verifications: Array, - @JvmField @Param(2) @Field(2) val extras: Bundle -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(VerifyPhoneNumberResponse::class.java) - } -} - -@SafeParcelable.Class -data class PhoneNumberVerification @Constructor constructor( - @JvmField @Param(1) @Field(1) val phoneNumber: String?, - @JvmField @Param(2) @Field(2) val timestampMillis: Long, - @JvmField @Param(3) @Field(3) val verificationMethod: Int, - @JvmField @Param(4) @Field(4) val simSlot: Int, - @JvmField @Param(5) @Field(5) val verificationToken: String?, - @JvmField @Param(6) @Field(6) val extras: Bundle?, - @JvmField @Param(7) @Field(7) val verificationStatus: Int, - @JvmField @Param(8) @Field(8) val retryAfterSeconds: Long -) : AbstractSafeParcelable() { - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(PhoneNumberVerification::class.java) - } -} \ No newline at end of file From aec46b8221eaf9a7e419bb65c426bec10870737c Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:42:52 -0400 Subject: [PATCH 09/63] Small fixes --- .../gms/constellation/core/AuthManager.kt | 2 +- .../core/GetVerifiedPhoneNumbers.kt | 2 +- .../constellation/core/IidTokenPhenotypes.kt | 10 +++++----- .../core/proto/builders/GaiaInfoBuilder.kt | 19 ++++++++++--------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt index ba601c3832..6f2a151427 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt @@ -56,7 +56,7 @@ class AuthManager private constructor(context: Context) { fun getIidToken(projectNumber: String? = null): String { return try { - val sender = projectNumber ?: IidTokenPhenotypes.DEFAULT_PROJECT_NUMBER.toString() + val sender = projectNumber ?: IidTokenPhenotypes.DEFAULT_PROJECT_NUMBER InstanceID.getInstance(context).getToken(sender, "GCM") } catch (_: Exception) { "" diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index b344d37d86..224e7a4f9a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -54,7 +54,7 @@ internal suspend fun fetchVerifiedPhoneNumbers( val certificateHash = bundle.getString("certificate_hash") ?: "" val tokenNonce = bundle.getString("token_nonce") ?: "" - val iidToken = authManager.getIidToken() + val iidToken = authManager.getIidToken(IidTokenPhenotypes.READ_ONLY_PROJECT_NUMBER) val iidTokenAuth = if (VerifyPhoneNumberApiPhenotypes.ENABLE_CLIENT_SIGNATURE) { val (signatureBytes, signTimestamp) = authManager.signIidToken(iidToken) IIDTokenAuth( diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/IidTokenPhenotypes.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/IidTokenPhenotypes.kt index 984c902321..4b42087420 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/IidTokenPhenotypes.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/IidTokenPhenotypes.kt @@ -1,9 +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 + 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" } \ No newline at end of file diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt index 60ff924562..bf76f353e6 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt @@ -55,12 +55,11 @@ operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? { @Suppress("DEPRECATION") suspend fun GaiaToken.Companion.getList(context: Context): List = withContext(Dispatchers.IO) { - val gaiaTokens = mutableListOf() - try { - val accounts = AccountManager.get(context).getAccountsByType("com.google") - if (accounts.isNotEmpty()) { + val accounts = AccountManager.get(context).getAccountsByType("com.google") + accounts.mapNotNull { account -> + try { val future = AccountManager.get(context).getAuthToken( - accounts.first(), + account, "oauth2:https://www.googleapis.com/auth/numberer", null, false, @@ -68,10 +67,12 @@ suspend fun GaiaToken.Companion.getList(context: Context): List = null ) val token = future.result?.getString(AccountManager.KEY_AUTHTOKEN) - if (!token.isNullOrBlank()) gaiaTokens.add(GaiaToken(token = token)) + + if (!token.isNullOrBlank()) GaiaToken(token = token) else null + + } catch (e: Exception) { + Log.w(TAG, "Could not retrieve Gaia token for an account", e) + null } - } catch (e: Exception) { - Log.w(TAG, "Could not retrieve Gaia tokens", e) } - gaiaTokens } From 23e80ba63c1028c896afbd0b8f2ee81ce65b2dbe Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:47:27 -0400 Subject: [PATCH 10/63] Formatting --- .../core/src/main/AndroidManifest.xml | 9 +++---- .../core/GetVerifiedPhoneNumbers.kt | 5 ++-- .../constellation/core/VerifyPhoneNumber.kt | 6 +++-- .../core/proto/builders/CommonBuilders.kt | 1 - .../core/proto/builders/GaiaInfoBuilder.kt | 1 - .../core/proto/builders/SyncRequestBuilder.kt | 11 +++++++-- .../core/verification/MoSmsVerifier.kt | 6 ++++- .../verification/RegisteredSmsVerifier.kt | 11 +++++++-- .../gms/constellation/GetIidTokenRequest.java | 5 ++-- .../constellation/GetIidTokenResponse.java | 8 ++----- .../GetPnvCapabilitiesRequest.java | 7 ++---- .../GetPnvCapabilitiesResponse.java | 20 +++++----------- .../gms/constellation/PhoneNumberInfo.java | 8 ++----- .../VerifyPhoneNumberRequest.java | 24 +++++-------------- .../VerifyPhoneNumberResponse.java | 18 ++++---------- 15 files changed, 59 insertions(+), 81 deletions(-) diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml index bd95282658..8ad6d5d3fe 100644 --- a/play-services-constellation/core/src/main/AndroidManifest.xml +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -6,16 +6,17 @@ + - - - + + diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index 224e7a4f9a..28f5df0669 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -1,4 +1,5 @@ @file:SuppressLint("NewApi") + package org.microg.gms.constellation.core import android.annotation.SuppressLint @@ -9,8 +10,8 @@ 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.PhoneNumberInfo -import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification import com.google.android.gms.constellation.VerifyPhoneNumberResponse +import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification import com.google.android.gms.constellation.internal.IConstellationCallbacks import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -19,8 +20,8 @@ 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 java.util.UUID import org.microg.gms.constellation.core.proto.VerifiedPhoneNumber +import java.util.UUID private const val TAG = "GetVerifiedPhoneNumbers" diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 32c5bd82d8..cf5871186e 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -10,11 +10,11 @@ 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.VerifyPhoneNumberRequest.IdTokenRequest import com.google.android.gms.constellation.PhoneNumberInfo -import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification 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 @@ -37,6 +37,7 @@ private enum class ReadCallbackMode { TYPED } +@Suppress("DEPRECATION") suspend fun handleVerifyPhoneNumberV1( context: Context, callbacks: IConstellationCallbacks, @@ -85,6 +86,7 @@ suspend fun handleVerifyPhoneNumberV1( ) } +@Suppress("DEPRECATION") suspend fun handleVerifyPhoneNumberSingleUse( context: Context, callbacks: IConstellationCallbacks, diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt index ae7e49ba83..d3a4e4beb5 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt @@ -1,4 +1,3 @@ - package org.microg.gms.constellation.core.proto.builders import android.content.Context diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt index bf76f353e6..4c6dd5987d 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt @@ -1,4 +1,3 @@ - package org.microg.gms.constellation.core.proto.builders import android.accounts.AccountManager diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt index 2be034dba2..3d1dca1acf 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt @@ -63,8 +63,14 @@ fun getTelephonyPhoneNumbers( ): List { 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 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() ?: return emptyList() @@ -122,6 +128,7 @@ suspend operator fun SyncRequest.Companion.invoke( ) } +@Suppress("DEPRECATION") suspend operator fun SyncRequest.Companion.invoke( context: Context, sessionId: String, diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index c1f746d109..2ab7176df7 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -170,7 +170,11 @@ suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse 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) { + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_PHONE_STATE + ) == PackageManager.PERMISSION_DENIED + ) { Log.e(TAG, "Permission not granted") return false } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt index aa8f8044b8..b010277b41 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt @@ -105,16 +105,23 @@ fun RegisteredSmsChallenge.verify(context: Context, subId: Int): ChallengeRespon } @SuppressLint("HardwareIds") +@Suppress("DEPRECATION") private fun getLocalNumbers(context: Context, targetSubId: Int): List { val numbers = linkedSetOf() val subscriptionManager = context.getSystemService() val telephonyManager = context.getSystemService() - val hasState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED + val hasState = ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_PHONE_STATE + ) == PackageManager.PERMISSION_GRANTED val isCarrier = telephonyManager?.hasCarrierPrivileges() == true val hasNumbers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_PHONE_NUMBERS + ) == PackageManager.PERMISSION_GRANTED } else { hasState } diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java index 5467842558..311dcb0fd9 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java @@ -11,6 +11,8 @@ @SafeParcelable.Class public class GetIidTokenRequest extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(GetIidTokenRequest.class); @Field(1) @Nullable public final Long projectNumber; @@ -26,7 +28,4 @@ public GetIidTokenRequest( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(GetIidTokenRequest.class); } \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java index 0c58b7238b..fd2a3c66d6 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java @@ -11,16 +11,15 @@ @SafeParcelable.Class public class GetIidTokenResponse extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter 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; @@ -41,7 +40,4 @@ public GetIidTokenResponse( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(GetIidTokenResponse.class); } \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java index aeafd2b357..9fbc98b5c0 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java @@ -12,12 +12,12 @@ @SafeParcelable.Class public class GetPnvCapabilitiesRequest extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(GetPnvCapabilitiesRequest.class); @Field(1) public final String policyId; - @Field(2) public final List verificationTypes; - @Field(3) public final List simSlotIndices; @@ -36,7 +36,4 @@ public GetPnvCapabilitiesRequest( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(GetPnvCapabilitiesRequest.class); } \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java index e5dea8c34c..b1382969ca 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java @@ -12,6 +12,8 @@ @SafeParcelable.Class public class GetPnvCapabilitiesResponse extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(GetPnvCapabilitiesResponse.class); @Field(1) public final List simCapabilities; @@ -27,24 +29,19 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(GetPnvCapabilitiesResponse.class); - @SafeParcelable.Class public static class SimCapability extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter 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 verificationCapabilities; @@ -67,17 +64,15 @@ public SimCapability( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(SimCapability.class); } @SafeParcelable.Class public static class VerificationCapability extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(VerificationCapability.class); @Field(1) public final int verificationMethod; - @Field(2) public final int statusValue; @@ -94,8 +89,5 @@ public VerificationCapability( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(VerificationCapability.class); } } \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java index 82b4ce0268..0dc2c9fbfe 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java @@ -12,16 +12,15 @@ @SafeParcelable.Class public class PhoneNumberInfo extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter 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; @@ -43,7 +42,4 @@ public PhoneNumberInfo( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(PhoneNumberInfo.class); } \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java index e554934ae6..c68ecc7e53 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java @@ -13,27 +13,22 @@ @SafeParcelable.Class public class VerifyPhoneNumberRequest extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter 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 targetedSims; - @Field(6) public final boolean silent; - @Field(7) public final int apiVersion; - @Field(8) public final List verificationMethodsValues; @@ -63,14 +58,12 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(VerifyPhoneNumberRequest.class); - @SafeParcelable.Class public static class IdTokenRequest extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(IdTokenRequest.class); @Field(1) public final String idToken; - @Field(2) public final String subscriberHash; @@ -87,16 +80,14 @@ public IdTokenRequest( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(IdTokenRequest.class); } @SafeParcelable.Class public static class ImsiRequest extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(ImsiRequest.class); @Field(1) public final String imsi; - @Field(2) public final String phoneNumberHint; @@ -113,8 +104,5 @@ public ImsiRequest( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(ImsiRequest.class); } } \ No newline at end of file diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java index 4a94badd74..d66fca5c19 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java @@ -12,9 +12,10 @@ @SafeParcelable.Class public class VerifyPhoneNumberResponse extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter CREATOR = + findCreator(VerifyPhoneNumberResponse.class); @Field(1) public final PhoneNumberVerification[] verifications; - @Field(2) public final Bundle extras; @@ -32,35 +33,27 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(VerifyPhoneNumberResponse.class); - @SafeParcelable.Class public static class PhoneNumberVerification extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter 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; @@ -89,8 +82,5 @@ public PhoneNumberVerification( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - - public static SafeParcelableCreatorAndWriter CREATOR = - findCreator(PhoneNumberVerification.class); } } \ No newline at end of file From 59e189566908bf8beeea334f3936555c5184f72f Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:01:21 -0400 Subject: [PATCH 11/63] why did i write it like this --- .../gms/constellation/core/proto/builders/GaiaInfoBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt index 4c6dd5987d..cb9c9e508f 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt @@ -41,7 +41,7 @@ operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? { GaiaSignalEntry( gaia_id = obfuscatedId, signal_type = GaiaAccountSignalType.GAIA_ACCOUNT_SIGNAL_AUTHENTICATED, - timestamp = Instant.ofEpochMilli(System.currentTimeMillis()) + timestamp = Instant.now() ) ) } From b20cc3caeded72faf3f7fd02bb95483b6f8a1f9d Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:39:24 -0400 Subject: [PATCH 12/63] Remove full name and use import instead --- .../gms/constellation/core/proto/builders/ClientInfoBuilder.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt index 8a67cb84ad..8ef77487f8 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt @@ -11,6 +11,7 @@ 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 @@ -157,7 +158,7 @@ suspend operator fun DroidGuardSignals.Companion.invoke(context: Context): Droid } return try { - val client = com.google.android.gms.droidguard.DroidGuard.getClient(context) + val client = DroidGuard.getClient(context) val data = mapOf( "package_name" to context.packageName, "timestamp" to System.currentTimeMillis().toString() From 2552a9e8937d28766ce17f25e1172c442f50b907 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:54:33 -0400 Subject: [PATCH 13/63] Use GMS package name instead of our own --- .../gms/constellation/core/GetVerifiedPhoneNumbers.kt | 3 ++- .../microg/gms/constellation/core/VerifyPhoneNumber.kt | 9 ++++++--- .../core/proto/builders/ClientInfoBuilder.kt | 2 +- .../core/proto/builders/SyncRequestBuilder.kt | 5 +++-- .../verification/ts43/ServiceEntitlementExtension.kt | 3 +-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index 28f5df0669..5f73229910 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -16,6 +16,7 @@ import com.google.android.gms.constellation.internal.IConstellationCallbacks 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 @@ -47,7 +48,7 @@ suspend fun handleGetVerifiedPhoneNumbers( internal suspend fun fetchVerifiedPhoneNumbers( context: Context, bundle: Bundle, - callingPackage: String = bundle.getString("calling_package") ?: context.packageName + callingPackage: String = bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME ): List = withContext(Dispatchers.IO) { val authManager = context.authManager val sessionId = UUID.randomUUID().toString() diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index cf5871186e..572d8dcf8f 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -20,6 +20,7 @@ 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.SyncRequest import org.microg.gms.constellation.core.proto.Verification import org.microg.gms.constellation.core.proto.builders.RequestBuildContext @@ -44,7 +45,8 @@ suspend fun handleVerifyPhoneNumberV1( bundle: Bundle, packageName: String? ) { - val callingPackage = packageName ?: bundle.getString("calling_package") ?: context.packageName + val callingPackage = + packageName ?: bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME val extras = Bundle(bundle).apply { putString("calling_package", callingPackage) putString("calling_api", "verifyPhoneNumber") @@ -93,7 +95,8 @@ suspend fun handleVerifyPhoneNumberSingleUse( bundle: Bundle, packageName: String? ) { - val callingPackage = packageName ?: bundle.getString("calling_package") ?: context.packageName + val callingPackage = + packageName ?: bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME val extras = Bundle(bundle).apply { putString("calling_package", callingPackage) putString("calling_api", "verifyPhoneNumberSingleUse") @@ -134,7 +137,7 @@ suspend fun handleVerifyPhoneNumberRequest( request: VerifyPhoneNumberRequest, packageName: String? ) { - val callingPackage = packageName ?: context.packageName + 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 diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt index 8ef77487f8..a456974288 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt @@ -160,7 +160,7 @@ suspend operator fun DroidGuardSignals.Companion.invoke(context: Context): Droid return try { val client = DroidGuard.getClient(context) val data = mapOf( - "package_name" to context.packageName, + "package_name" to Constants.GMS_PACKAGE_NAME, "timestamp" to System.currentTimeMillis().toString() ) val result = client.getResults("constellation_verify", data, null).await() diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt index 3d1dca1acf..0dc25d93b6 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt @@ -17,6 +17,7 @@ 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 @@ -113,7 +114,7 @@ suspend operator fun SyncRequest.Companion.invoke( sessionId: String, request: VerifyPhoneNumberRequest, includeClientAuth: Boolean = false, - callingPackage: String = context.packageName, + callingPackage: String = Constants.GMS_PACKAGE_NAME, triggerType: RequestTrigger.Type = RequestTrigger.Type.TRIGGER_API_CALL ): SyncRequest { val buildContext = buildRequestContext(context) @@ -136,7 +137,7 @@ suspend operator fun SyncRequest.Companion.invoke( buildContext: RequestBuildContext, imsiToInfoMap: Map = buildImsiToSubscriptionInfoMap(context), includeClientAuth: Boolean = false, - callingPackage: String = context.packageName, + callingPackage: String = Constants.GMS_PACKAGE_NAME, triggerType: RequestTrigger.Type = RequestTrigger.Type.TRIGGER_API_CALL ): SyncRequest { val apiParamsList = Param.getList(request.extras) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt index 56d8114022..f5966f6691 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ts43/ServiceEntitlementExtension.kt @@ -32,9 +32,8 @@ fun ServiceEntitlementRequest.builder( ) fun ServiceEntitlementRequest.userAgent(context: Context): String { - val packageVersion = runCatching { + val packageVersion = context.packageManager.getPackageInfo(context.packageName, 0).versionName.orEmpty() - }.getOrDefault("") val vendor = terminal_vendor.take(4) val model = terminal_model.take(10) val swVersion = terminal_software_version.take(20) From d08e4d8cb3f92c5d97f6b0e3e8e7a620ab50fbec Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:07:57 -0400 Subject: [PATCH 14/63] Remove unnecessary dependencies --- play-services-constellation/build.gradle | 9 +-------- play-services-constellation/core/build.gradle | 4 +--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/play-services-constellation/build.gradle b/play-services-constellation/build.gradle index e0cc981be3..f2bacc16a6 100644 --- a/play-services-constellation/build.gradle +++ b/play-services-constellation/build.gradle @@ -5,9 +5,6 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' apply plugin: 'signing' android { @@ -30,10 +27,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - - kotlinOptions { - jvmTarget = 1.8 - } } apply from: '../gradle/publish-android.gradle' @@ -43,5 +36,5 @@ description = 'microG implementation of play-services-constellation' dependencies { implementation project(':play-services-basement') - kapt project(":safe-parcel-processor") + annotationProcessor project(":safe-parcel-processor") } diff --git a/play-services-constellation/core/build.gradle b/play-services-constellation/core/build.gradle index c51c1f39ea..cd7df4f4db 100644 --- a/play-services-constellation/core/build.gradle +++ b/play-services-constellation/core/build.gradle @@ -6,8 +6,6 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' apply plugin: 'signing' apply plugin: 'com.squareup.wire' @@ -58,7 +56,7 @@ dependencies { 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" - api "com.squareup.okhttp3:okhttp:$okHttpVersion" } From 05cd22d05449097c6d6b12cccf318e0e6496075b Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:54:18 -0400 Subject: [PATCH 15/63] Refactor package name from `builders` to `builder` --- .../microg/gms/constellation/core/VerifyPhoneNumber.kt | 8 ++++---- .../core/proto/{builders => builder}/ClientInfoBuilder.kt | 2 +- .../core/proto/{builders => builder}/CommonBuilders.kt | 2 +- .../core/proto/{builders => builder}/GaiaInfoBuilder.kt | 2 +- .../proto/{builders => builder}/RequestBuildContext.kt | 2 +- .../proto/{builders => builder}/SyncRequestBuilder.kt | 2 +- .../proto/{builders => builder}/TelephonyInfoBuilder.kt | 2 +- .../constellation/core/verification/ChallengeProcessor.kt | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) rename play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/{builders => builder}/ClientInfoBuilder.kt (99%) rename play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/{builders => builder}/CommonBuilders.kt (97%) rename play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/{builders => builder}/GaiaInfoBuilder.kt (98%) rename play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/{builders => builder}/RequestBuildContext.kt (90%) rename play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/{builders => builder}/SyncRequestBuilder.kt (99%) rename play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/{builders => builder}/TelephonyInfoBuilder.kt (99%) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 572d8dcf8f..afdb361a67 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -23,10 +23,10 @@ import kotlinx.coroutines.withContext import org.microg.gms.common.Constants import org.microg.gms.constellation.core.proto.SyncRequest import org.microg.gms.constellation.core.proto.Verification -import org.microg.gms.constellation.core.proto.builders.RequestBuildContext -import org.microg.gms.constellation.core.proto.builders.buildImsiToSubscriptionInfoMap -import org.microg.gms.constellation.core.proto.builders.buildRequestContext -import org.microg.gms.constellation.core.proto.builders.invoke +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.invoke import org.microg.gms.constellation.core.verification.ChallengeProcessor import java.util.UUID diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt similarity index 99% rename from play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt index a456974288..e3265a57b1 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt @@ -1,6 +1,6 @@ @file:RequiresApi(Build.VERSION_CODES.O) -package org.microg.gms.constellation.core.proto.builders +package org.microg.gms.constellation.core.proto.builder import android.annotation.SuppressLint import android.content.Context diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt similarity index 97% rename from play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt index d3a4e4beb5..b34b11010f 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/CommonBuilders.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.core.proto.builders +package org.microg.gms.constellation.core.proto.builder import android.content.Context import android.os.Build diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/GaiaInfoBuilder.kt similarity index 98% rename from play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/GaiaInfoBuilder.kt index cb9c9e508f..6eca28cdf7 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/GaiaInfoBuilder.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.core.proto.builders +package org.microg.gms.constellation.core.proto.builder import android.accounts.AccountManager import android.content.Context diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/RequestBuildContext.kt similarity index 90% rename from play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/RequestBuildContext.kt index 1262045264..951255aa86 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/RequestBuildContext.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/RequestBuildContext.kt @@ -1,4 +1,4 @@ -package org.microg.gms.constellation.core.proto.builders +package org.microg.gms.constellation.core.proto.builder import android.content.Context import org.microg.gms.constellation.core.AuthManager diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt similarity index 99% rename from play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt index 0dc25d93b6..d58eaeb8f7 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt @@ -1,7 +1,7 @@ @file:RequiresApi(Build.VERSION_CODES.O) @file:SuppressLint("HardwareIds") -package org.microg.gms.constellation.core.proto.builders +package org.microg.gms.constellation.core.proto.builder import android.Manifest import android.annotation.SuppressLint diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/TelephonyInfoBuilder.kt similarity index 99% rename from play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt rename to play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/TelephonyInfoBuilder.kt index a66a562957..307ae6a24e 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builders/TelephonyInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/TelephonyInfoBuilder.kt @@ -2,7 +2,7 @@ @file:SuppressLint("HardwareIds") @file:Suppress("DEPRECATION") -package org.microg.gms.constellation.core.proto.builders +package org.microg.gms.constellation.core.proto.builder import android.annotation.SuppressLint import android.content.Context diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index f01997d252..61105217b2 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -18,8 +18,8 @@ import org.microg.gms.constellation.core.proto.RequestHeader import org.microg.gms.constellation.core.proto.RequestTrigger import org.microg.gms.constellation.core.proto.Verification import org.microg.gms.constellation.core.proto.VerificationMethod -import org.microg.gms.constellation.core.proto.builders.RequestBuildContext -import org.microg.gms.constellation.core.proto.builders.invoke +import org.microg.gms.constellation.core.proto.builder.RequestBuildContext +import org.microg.gms.constellation.core.proto.builder.invoke object ChallengeProcessor { private const val TAG = "ChallengeProcessor" From 117312df213e08ab08eba7329573697565a2875f Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:53:47 -0400 Subject: [PATCH 16/63] Clean up `constellation.proto` --- .../core/src/main/proto/constellation.proto | 125 +++++++++++++----- 1 file changed, 93 insertions(+), 32 deletions(-) diff --git a/play-services-constellation/core/src/main/proto/constellation.proto b/play-services-constellation/core/src/main/proto/constellation.proto index 93a10c652c..de4e950b6e 100644 --- a/play-services-constellation/core/src/main/proto/constellation.proto +++ b/play-services-constellation/core/src/main/proto/constellation.proto @@ -5,7 +5,6 @@ package google.internal.communications.phonedeviceverification.v1; import "google/protobuf/timestamp.proto"; -// Verification and consent RPCs. service PhoneDeviceVerification { rpc GetConsent(GetConsentRequest) returns (GetConsentResponse); rpc SetConsent(SetConsentRequest) returns (SetConsentResponse); @@ -13,30 +12,31 @@ service PhoneDeviceVerification { rpc Proceed(ProceedRequest) returns (ProceedResponse); } -// Read-only phone number retrieval RPC. service PhoneNumber { rpc GetVerifiedPhoneNumbers(GetVerifiedPhoneNumbersRequest) returns (GetVerifiedPhoneNumbersResponse); } -// Shared request and response messages. -// Header for each client request. message RequestHeader { ClientInfo client_info = 1; ClientAuth client_auth = 2; string session_id = 3; RequestTrigger trigger = 4; } + message ResponseHeader { StatusProtoSimple status = 1; } + message StatusProtoSimple { int32 code = 1; } + enum StatusCode { STATUS_CODE_UNSPECIFIED = 0; STATUS_CODE_OK = 1; STATUS_CODE_ERROR = 2; } + message ClientInfo { DeviceID device_id = 1; bytes client_public_key = 2; @@ -78,25 +78,30 @@ enum DeviceType { DEVICE_TYPE_DESKTOP = 9; DEVICE_TYPE_XR_PERIPHERAL = 10; } + message ClientAuth { DeviceID device_id = 1; bytes signature = 2; google.protobuf.Timestamp sign_timestamp = 3; } + message CountryInfo { repeated string sim_countries = 1; repeated string network_countries = 2; } + message DroidGuardSignals { string droidguard_result = 1; string droidguard_token = 2; } + message DeviceID { string iid_token = 1; int64 primary_device_id = 2; int64 user_serial = 3; int64 gms_android_id = 4; } + message RequestTrigger { enum Type { UNKNOWN = 0; @@ -115,20 +120,25 @@ message RequestTrigger { } Type type = 1; } + message DroidGuardAttestation { string droidguard_result = 1; string droidguard_token = 2; } + message Experiment { string key = 1; string value = 2; } + message GaiaID { string id = 1; } + message GaiaToken { string token = 1; } + message GaiaSignalEntry { string gaia_id = 1; GaiaAccountSignalType signal_type = 2; @@ -141,14 +151,15 @@ enum GaiaAccountSignalType { GAIA_ACCOUNT_SIGNAL_UNAUTHENTICATED = 2; GAIA_ACCOUNT_SIGNAL_REMOVED_WITH_GMS_AUTH_BROADCAST = 3; } + message GaiaSignals { repeated GaiaSignalEntry gaia_signals = 1; } + message Status { int32 code = 1; } -// Named API params used by verification RPCs. message VerificationPolicy { string policy_id = 1; int64 max_verification_age_hours = 2; @@ -157,20 +168,22 @@ message VerificationPolicy { repeated VerificationParam params = 5; } -// Parameters for ID token generation. message IdTokenRequest { string certificate_hash = 1; string token_nonce = 2; } + message VerificationParam { string key = 1; string value = 2; } + message SimOperatorInfo { string imsi_hash = 1; string sim_operator = 3; } + message NetworkSignal { enum Type { TYPE_UNKNOWN = 0; @@ -194,6 +207,7 @@ message NetworkSignal { State state = 2; Availability availability = 3; } + message MobileOperatorInfo { string country_code = 1; uint64 nil_since_usec = 2; @@ -202,12 +216,12 @@ message MobileOperatorInfo { string operator_name = 5; } -// Sync and proceed flow. message SyncRequest { repeated Verification verifications = 3; RequestHeader header = 4; repeated VerificationToken verification_tokens = 5; } + message SyncResponse { repeated VerificationResponse responses = 1; ServerTimestamp next_sync_time = 2; @@ -215,11 +229,13 @@ message SyncResponse { DroidguardToken droidguard_token = 4; repeated VerificationToken verification_tokens = 5; } + message ProceedRequest { Verification verification = 2; ChallengeResponse challenge_response = 3; RequestHeader header = 4; } + message ProceedResponse { Verification verification = 1; ResponseHeader header = 2; @@ -227,17 +243,16 @@ message ProceedResponse { DroidguardToken droidguard_token = 4; } -// Server time plus the time when the server wrote this message. message ServerTimestamp { google.protobuf.Timestamp timestamp = 1; google.protobuf.Timestamp now = 2; } + message VerificationToken { bytes token = 1; google.protobuf.Timestamp expiration_time = 2; } -// Verification state and challenge model. message Verification { enum State { UNKNOWN = 0; @@ -273,18 +288,22 @@ message Verification { ChallengePreference challenge_preference = 7; VerificationPolicy structured_api_params = 8; } + message VerificationResponse { Verification verification = 1; StatusProto error = 2; } + message StatusProto { int32 code = 1; string message = 3; } + message SIMSlotInfo { int32 slot_index = 1; int32 subscription_id = 2; } + message SIMAssociation { message SIMInfo { @@ -303,8 +322,10 @@ message SIMAssociation { repeated GaiaToken gaia_tokens = 2; SIMSlotInfo sim_slot = 4; } + message GaiaAssociation { } + message VerificationAssociation { oneof association { SIMAssociation sim = 1; @@ -312,18 +333,16 @@ message VerificationAssociation { } } -// Information for a verified record. message VerificationInfo { string phone_number = 1; google.protobuf.Timestamp verification_time = 2; VerificationMethod challenge_method = 6; } + message PendingVerificationInfo { Challenge challenge = 2; } -// Challenge payloads and responses. -// Challenge issued by the server. message Challenge { ChallengeID challenge_id = 1; VerificationMethod type = 2; @@ -336,24 +355,25 @@ message Challenge { Ts43Challenge ts43_challenge = 12; } -// Carrier ID challenge used for SS7 traffic. message CarrierIdChallenge { string isim_request = 3; int32 auth_type = 5; int32 app_type = 6; } + message MTChallenge { string sms = 1; } + message ChallengeID { string id = 1; } + message Capabilities { string droidguard_result = 1; string droidguard_token = 2; } -// MO SMS challenge. message MoChallenge { string proxy_number = 1; DataSmsInfo data_sms_info = 3; @@ -361,15 +381,19 @@ message MoChallenge { string polling_intervals = 5; string sms_without_persisting = 6; } + message DataSmsInfo { int32 destination_port = 1; } + message RegisteredSmsChallenge { repeated PhoneNumberID verified_senders = 1; } + message PhoneNumberID { bytes phone_number_id = 1; } + message FlashCallChallenge { repeated PhoneRange phone_ranges = 1; FlashCallState state = 2; @@ -377,6 +401,7 @@ message FlashCallChallenge { repeated ChallengeResponse previous_challenge_responses = 4; int64 millis_between_interceptions = 5; } + message PhoneRange { string phone_number_prefix = 1; string phone_number_suffix = 2; @@ -391,6 +416,7 @@ enum FlashCallState { FLASH_CALL_STATE_VERIFIED = 3; FLASH_CALL_STATE_FAILED = 4; } + message UnverifiedInfo { enum Reason { UNKNOWN_REASON = 0; @@ -408,7 +434,6 @@ message UnverifiedInfo { VerificationMethod challenge_method = 3; } -// Challenge response. message ChallengeResponse { MTChallengeResponseData mt_response = 1; CarrierIdChallengeResponse carrier_id_response = 2; @@ -417,10 +442,12 @@ message ChallengeResponse { FlashCallChallengeResponse flash_call_response = 7; Ts43ChallengeResponse ts43_challenge_response = 9; } + message MTChallengeResponseData { string sms = 1; string sender = 2; } + message CarrierIdChallengeResponse { string isim_response = 3; CarrierIdError carrier_id_error = 4; @@ -440,6 +467,7 @@ enum CarrierIdError { CARRIER_ID_ERROR_INTERNAL_ERROR = 10; CARRIER_ID_ERROR_INVALID_ARGUMENT = 11; } + message MOChallengeResponseData { enum Status { UNKNOWN_STATUS = 0; @@ -452,15 +480,19 @@ message MOChallengeResponseData { int64 sms_result_code = 2; int64 sms_error_code = 3; } + message RegisteredSmsChallengeResponse { repeated RegisteredSmsChallengeResponseItem items = 1; } + message RegisteredSmsChallengeResponseItem { RegisteredSmsChallengeResponsePayload payload = 2; } + message RegisteredSmsChallengeResponsePayload { bytes payload = 1; } + message Ts43ChallengeResponse { Ts43Type ts43_type = 1; ClientChallengeResponse client_challenge_response = 2; @@ -468,6 +500,7 @@ message Ts43ChallengeResponse { Ts43ChallengeResponseStatus status = 4; repeated string http_history = 5; } + message Ts43ChallengeResponseStatus { enum Code { TS43_STATUS_UNSPECIFIED = 0; @@ -484,8 +517,6 @@ message Ts43ChallengeResponseStatus { } } -// The numeric 1..9 range is intentionally left generic because those values are -// overloaded across multiple TS.43 request stages. message Ts43ChallengeResponseError { enum Code { TS43_ERROR_CODE_UNSPECIFIED = 0; @@ -521,12 +552,15 @@ message Ts43ChallengeResponseError { int32 http_status = 2; RequestType request_type = 3; } + message ServerChallengeResponse { string acquire_temporary_token_response = 2; } + message ClientChallengeResponse { string get_phone_number_response = 2; } + message FlashCallChallengeResponse { enum Error { NO_ERROR = 0; @@ -549,6 +583,7 @@ message FlashCallChallengeResponse { string caller = 1; Error error = 2; } + message Ts43Type { enum Integrator { TS43_INTEGRATOR_UNSPECIFIED = 0; @@ -571,6 +606,7 @@ message Ts43Type { } Integrator integrator = 1; } + message CellularNetworkEvent { google.protobuf.Timestamp timestamp = 1; bool mobile_data_enabled = 2; @@ -579,10 +615,12 @@ message CellularNetworkEvent { bool mobile_data_always_on = 5; repeated NetworkState networks = 6; } + message NetworkState { repeated int32 types = 1; bool available = 2; } + message ServiceStateEvent { google.protobuf.Timestamp timestamp = 1; bool airplane_mode_enabled = 2; @@ -593,11 +631,13 @@ message ServiceStateEvent { int32 data_network_type = 7; int32 signal_strength = 8; } + message SMSEvent { google.protobuf.Timestamp timestamp = 1; EventDirection direction = 2; EventPhoneNumberType number_type = 3; } + message CallEvent { google.protobuf.Timestamp timestamp = 1; EventDirection direction = 2; @@ -615,6 +655,7 @@ enum EventPhoneNumberType { LONG_NUMBER = 1; SHORT_CODE = 2; } + message TelephonyInfo { enum PhoneType { PHONE_TYPE_UNKNOWN = 0; @@ -680,18 +721,19 @@ message TelephonyInfo { ToggleState carrier_id_capability = 23; int64 sim_carrier_id = 25; } + message SimNetworkInfo { string country_iso = 1; string operator = 2; string operator_name = 3; int32 inactive_time_diff_ms = 4; } + message DroidguardToken { string token = 1; google.protobuf.Timestamp ttl = 2; } -// Consent and Asterism messages. message GetConsentRequest { DeviceID device_id = 1; repeated GaiaToken gaia_tokens = 2; @@ -703,6 +745,7 @@ message GetConsentRequest { string session_id = 9; bool unknown_flag = 10; } + message GetConsentResponse { RcsConsent rcs_consent = 1; ServerTimestamp next_check_time = 5; @@ -712,8 +755,6 @@ message GetConsentResponse { PermissionState permission_state = 10; } -// These values intentionally alias because the same field is reused across -// multiple consent surfaces. enum FlowContext { option allow_alias = true; FLOW_CONTEXT_UNSPECIFIED = 0; @@ -724,6 +765,7 @@ enum FlowContext { FLOW_CONTEXT_RCS_DEFAULT_ON_LEGAL_FYI_IN_SETTINGS = 4; FLOW_CONTEXT_RCS_UNKNOWN_5 = 5; } + message SetConsentRequest { RequestHeader header = 1; RcsConsent rcs_consent = 3; @@ -736,6 +778,7 @@ message SetConsentRequest { AsterismConsent device_consent = 10; } } + message SetConsentResponse { } @@ -746,8 +789,6 @@ enum Consent { EXPIRED = 3; } -// These values intentionally alias because RCS and device consent versions -// share the same field. enum ConsentVersion { option allow_alias = true; CONSENT_VERSION_UNSPECIFIED = 0; @@ -759,41 +800,47 @@ enum ConsentVersion { PHONE_VERIFICATION_INTL_SMS_CALLS = 3; PHONE_VERIFICATION_REACHABILITY_INTL_SMS_CALLS = 4; } + message AsterismConsent { Consent consent = 1; ConsentSource consent_source = 2; ConsentVersion consent_version = 3; } + message ConsentState { Consent state = 2; } -// `flags` is still an opaque server-defined bitfield. message PermissionState { int32 flags = 1; PermissionType type = 2; } + enum PermissionType { PERMISSION_TYPE_UNSPECIFIED = 0; LEGACY_DPNV = 1; PNVR = 2; NOT_ALLOWED = 3; } + message RcsConsent { Consent consent = 2; ConsentVersion consent_version = 3; } + message GaiaConsent { AsterismClient asterism_client = 1; Consent consent = 2; ConsentVersion consent_version = 3; } + message OnDemandConsent { Consent consent = 1; GaiaToken gaia_token = 2; string consent_variant = 3; string trigger = 4; } + enum ConsentSource { SOURCE_UNSPECIFIED = 0; ANDROID_DEVICE_SETTINGS = 1; @@ -805,6 +852,7 @@ enum ConsentSource { MEET_ON_DEMAND_CONSENT = 7; GPAY_ON_DEMAND_CONSENT = 8; } + enum AsterismClient { UNKNOWN_CLIENT = 0; CONSTELLATION = 1; @@ -812,8 +860,6 @@ enum AsterismClient { ONE_TIME_VERIFICATION = 3; } -// Read-only phone number RPC. -// Request message for GetVerifiedPhoneNumbers. message GetVerifiedPhoneNumbersRequest { enum PhoneNumberSelection { SELECTION_UNSPECIFIED = 0; @@ -830,20 +876,22 @@ message GetVerifiedPhoneNumbersRequest { RequestInfo request_info = 7; } -// Response message for GetVerifiedPhoneNumbers. message GetVerifiedPhoneNumbersResponse { repeated VerifiedPhoneNumber phone_numbers = 2; } + message IIDTokenAuth { string iid_token = 1; bytes client_sign = 2; google.protobuf.Timestamp sign_timestamp = 3; } + message TokenOption { string certificate_hash = 1; string token_nonce = 2; string package_name = 3; } + message VerifiedPhoneNumber { string phone_number = 1; google.protobuf.Timestamp verification_time = 2; @@ -863,8 +911,6 @@ enum TelephonyPhoneNumberType { PHONE_NUMBER_SOURCE_IMS = 3; } -// Shared helpers. -// Data consistency requirement for read-only RPCs. message ConsistencyOption { enum Consistency { CONSISTENCY_UNSPECIFIED = 0; @@ -874,23 +920,24 @@ message ConsistencyOption { Consistency consistency = 1; } -// Request metadata for analytics and legacy policy attribution. message RequestInfo { string policy_id = 1; } + message Param { string key = 1; string value = 2; } -// Client challenge preference. message ChallengePreference { repeated VerificationMethod capabilities = 1; ChallengePreferenceMetadata metadata = 2; } + message ChallengePreferenceMetadata { string sms_signature = 2; } + enum VerificationMethod { UNKNOWN = 0; MO_SMS = 1; @@ -902,31 +949,36 @@ enum VerificationMethod { TS43 = 11; } -// Audit payloads. message AuditToken { AuditTokenMetadata metadata = 2; } + message AuditTokenMetadata { AuditUuid uuid = 1; } + message AuditUuid { int64 uuid_msb = 1; int64 uuid_lsb = 2; } + message AuditPayload { AuditDeviceInfo device_info = 1; AuditDeviceId device_id = 2; AuditEventMetadata event_metadata = 10; } + message AuditDeviceInfo { oneof device_identifier { string android_id_hash = 1; string instance_id = 2; } } + message AuditDeviceId { string android_id_hash = 1; } + message AuditEventMetadata { AuditEventType event_type = 1; string session_id = 2; @@ -941,32 +993,37 @@ message AuditEventMetadata { AuditComponentInfo component_info = 11; string trigger = 12; } + enum AuditEventType { EVENT_TYPE_UNSPECIFIED = 0; ASTERISM_CONSENT_CHANGE = 1; RCS_CONSENT_CHANGE = 2; VERIFICATION_COMPLETE = 3; } + message AuditConsentDetails { repeated int32 sim_slot_ids = 1; } + message AuditComponentInfo { AuditComponentType component_type = 1; } + enum AuditComponentType { COMPONENT_UNSPECIFIED = 0; ASTERISM_CONSTELLATION = 119; ASTERISM_RCS = 120; } + message AuditConsentState { Consent state = 1; } + enum AuditEventTypeValue { EVENT_VALUE_UNSPECIFIED = 0; ASTERISM_CLIENT_CONSENT_CHANGE = 187; } -// TS.43 challenge messages. message Ts43Challenge { Ts43Type ts43_type = 1; string entitlement_url = 2; @@ -976,12 +1033,15 @@ message Ts43Challenge { string app_id = 6; string eap_aka_realm = 7; } + message ServerChallenge { OdsaOperation operation = 1; } + message ClientChallenge { OdsaOperation operation = 1; } + message OdsaOperation { string operation = 1; int32 operation_type = 2; @@ -1005,6 +1065,7 @@ message OdsaOperation { string old_terminal_id = 20; string old_terminal_iccid = 21; } + message ServiceEntitlementRequest { int32 notification_action = 1; string entitlement_version = 2; From db60565d3822c9da99d6d53037dde1960800b731 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 29 Mar 2026 04:16:31 -0400 Subject: [PATCH 17/63] More consistent formatting --- .../android/gms/constellation/GetIidTokenRequest.java | 1 + .../android/gms/constellation/GetIidTokenResponse.java | 4 ++++ .../gms/constellation/GetPnvCapabilitiesRequest.java | 3 +++ .../gms/constellation/GetPnvCapabilitiesResponse.java | 1 + .../google/android/gms/constellation/PhoneNumberInfo.java | 4 ++++ .../gms/constellation/VerifyPhoneNumberRequest.java | 8 ++++++++ .../gms/constellation/VerifyPhoneNumberResponse.java | 2 ++ 7 files changed, 23 insertions(+) diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java index 311dcb0fd9..c24de63130 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenRequest.java @@ -13,6 +13,7 @@ public class GetIidTokenRequest extends AbstractSafeParcelable { public static SafeParcelableCreatorAndWriter CREATOR = findCreator(GetIidTokenRequest.class); + @Field(1) @Nullable public final Long projectNumber; diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java index fd2a3c66d6..19b97ec9cc 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetIidTokenResponse.java @@ -13,13 +13,17 @@ public class GetIidTokenResponse extends AbstractSafeParcelable { public static SafeParcelableCreatorAndWriter 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; diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java index 9fbc98b5c0..a0c13b3306 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesRequest.java @@ -14,10 +14,13 @@ public class GetPnvCapabilitiesRequest extends AbstractSafeParcelable { public static SafeParcelableCreatorAndWriter CREATOR = findCreator(GetPnvCapabilitiesRequest.class); + @Field(1) public final String policyId; + @Field(2) public final List verificationTypes; + @Field(3) public final List simSlotIndices; diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java index b1382969ca..ee522b446e 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/GetPnvCapabilitiesResponse.java @@ -14,6 +14,7 @@ public class GetPnvCapabilitiesResponse extends AbstractSafeParcelable { public static SafeParcelableCreatorAndWriter CREATOR = findCreator(GetPnvCapabilitiesResponse.class); + @Field(1) public final List simCapabilities; diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java index 0dc2c9fbfe..77f4fd7753 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/PhoneNumberInfo.java @@ -14,13 +14,17 @@ public class PhoneNumberInfo extends AbstractSafeParcelable { public static SafeParcelableCreatorAndWriter 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; diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java index c68ecc7e53..8c2327f091 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java @@ -15,20 +15,28 @@ public class VerifyPhoneNumberRequest extends AbstractSafeParcelable { public static SafeParcelableCreatorAndWriter 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 targetedSims; + @Field(6) public final boolean silent; + @Field(7) public final int apiVersion; + @Field(8) public final List verificationMethodsValues; diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java index d66fca5c19..263461debf 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberResponse.java @@ -14,8 +14,10 @@ public class VerifyPhoneNumberResponse extends AbstractSafeParcelable { public static SafeParcelableCreatorAndWriter CREATOR = findCreator(VerifyPhoneNumberResponse.class); + @Field(1) public final PhoneNumberVerification[] verifications; + @Field(2) public final Bundle extras; From 0621ee71712dc07a3ee0b98142da033d553c4fa9 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:19:31 -0400 Subject: [PATCH 18/63] Use actual Gaia ID --- .../core/src/main/AndroidManifest.xml | 1 + .../core/proto/builder/GaiaInfoBuilder.kt | 70 +++++++++++-------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml index 8ad6d5d3fe..aa877ea5e5 100644 --- a/play-services-constellation/core/src/main/AndroidManifest.xml +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ xmlns:tools="http://schemas.android.com/tools"> + diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/GaiaInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/GaiaInfoBuilder.kt index 6eca28cdf7..9c7c7134b0 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/GaiaInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/GaiaInfoBuilder.kt @@ -12,52 +12,60 @@ 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 -import java.security.MessageDigest -import kotlin.math.abs -import kotlin.math.absoluteValue private const val TAG = "GaiaInfoBuilder" @RequiresApi(Build.VERSION_CODES.O) -operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? { - val entries = mutableListOf() - try { - val accounts = AccountManager.get(context).getAccountsByType("com.google") - val md = MessageDigest.getInstance("SHA-256") +suspend operator fun GaiaSignals.Companion.invoke(context: Context): GaiaSignals? = + withContext(Dispatchers.IO) { + val entries = mutableListOf() + val accountManager = AccountManager.get(context) - for (account in accounts) { - // Note: Simple implementation, maybe do actual obfuscated Gaia ID retrieval later? - val hash = md.digest(account.name.toByteArray(Charsets.UTF_8)) - val number = - hash.take(8).fold(0L) { acc, byte -> (acc shl 8) or (byte.toLong() and 0xFF) } - val obfuscatedId = try { - number.absoluteValue.toString() - } catch (_: Exception) { - abs(account.name.hashCode().toLong()).toString() - } + try { + val accounts = accountManager.getAccountsByType("com.google") - entries.add( - GaiaSignalEntry( - gaia_id = obfuscatedId, - signal_type = GaiaAccountSignalType.GAIA_ACCOUNT_SIGNAL_AUTHENTICATED, - timestamp = Instant.now() + 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) } - } catch (e: Exception) { - Log.w(TAG, "Could not build Gaia signals", e) + if (entries.isNotEmpty()) GaiaSignals(gaia_signals = entries) else null } - return if (entries.isNotEmpty()) GaiaSignals(gaia_signals = entries) else null -} @Suppress("DEPRECATION") suspend fun GaiaToken.Companion.getList(context: Context): List = withContext(Dispatchers.IO) { - val accounts = AccountManager.get(context).getAccountsByType("com.google") + val accountManager = AccountManager.get(context) + val accounts = accountManager.getAccountsByType("com.google") accounts.mapNotNull { account -> try { - val future = AccountManager.get(context).getAuthToken( + val future = accountManager.getAuthToken( account, "oauth2:https://www.googleapis.com/auth/numberer", null, @@ -70,7 +78,7 @@ suspend fun GaiaToken.Companion.getList(context: Context): List = if (!token.isNullOrBlank()) GaiaToken(token = token) else null } catch (e: Exception) { - Log.w(TAG, "Could not retrieve Gaia token for an account", e) + Log.w(TAG, "Could not retrieve Gaia token for account ${account.name}", e) null } } From b82676c0d9b90804b45a186dbdb0e9fdba79fe65 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:52:42 -0400 Subject: [PATCH 19/63] Align more with official behavior --- .../core/VerificationMappings.kt | 87 ++++++++++++------- .../constellation/core/VerifyPhoneNumber.kt | 27 ++++-- .../core/verification/ChallengeProcessor.kt | 6 +- .../VerifyPhoneNumberRequest.java | 6 +- 4 files changed, 79 insertions(+), 47 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt index 2fae9c903b..edf9a5de0b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerificationMappings.kt @@ -11,7 +11,7 @@ import org.microg.gms.constellation.core.proto.VerificationMethod fun UnverifiedInfo.Reason.toVerificationStatus(): Verification.Status { return when (this) { - UnverifiedInfo.Reason.UNKNOWN_REASON -> Verification.Status.STATUS_PENDING + 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 @@ -25,72 +25,76 @@ fun UnverifiedInfo.Reason.toVerificationStatus(): Verification.Status { } } -fun Verification.getState(): Verification.State { - return when { +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 } -} -fun Verification.getVerificationStatus(): Verification.Status { - return when (getState()) { +val Verification.effectiveStatus: Verification.Status + get() = when (state) { Verification.State.VERIFIED -> Verification.Status.STATUS_VERIFIED - - Verification.State.PENDING -> { + 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_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): PhoneNumberVerification { - val verificationStatus = this.getVerificationStatus() - var phoneNumber: String? = null - var timestampMillis = System.currentTimeMillis() - var verificationMethod = VerificationMethod.UNKNOWN - var retryAfterSeconds = 0L + 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 - when (this.getState()) { + var phoneNumber = "" + var timestampMillis = 0L + var verificationMethod = VerificationMethod.UNKNOWN + var verificationToken: String? = null + var retryAfterSeconds = -1L + + when (state) { Verification.State.VERIFIED -> { - val info = this.verification_info - phoneNumber = info?.phone_number - timestampMillis = info?.verification_time?.toEpochMilli() ?: System.currentTimeMillis() - verificationMethod = info?.challenge_method ?: VerificationMethod.UNKNOWN + 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 = - this.pending_verification_info?.challenge?.type ?: VerificationMethod.UNKNOWN + pending_verification_info?.challenge?.type ?: VerificationMethod.UNKNOWN } Verification.State.NONE -> { - val info = this.unverified_info + 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) - } ?: 0L + } ?: -1L } - else -> {} + Verification.State.UNKNOWN -> Unit } - val simImsi = this.association?.sim?.sim_info?.imsi?.firstOrNull() - val simSlot = if (simImsi != null) imsiToSlotMap[simImsi] ?: 0 else 0 - val verificationToken = extras.getString("id_token") + extras.remove("verification_method") + extras.remove("sim_slot_index") return PhoneNumberVerification( phoneNumber, @@ -99,7 +103,7 @@ fun Verification.toClientVerification(imsiToSlotMap: Map): PhoneNum simSlot, verificationToken, extras, - verificationStatus.value, + clientStatus, retryAfterSeconds ) } @@ -112,7 +116,6 @@ private fun Verification.buildClientExtras(): Bundle { val slotIndex = association?.sim?.sim_slot?.slot_index if (slotIndex != null && slotIndex >= 0) { - // GMS exposes this as a string inside the extras bundle. bundle.putString("sim_slot_index", slotIndex.toString()) } return bundle @@ -130,8 +133,26 @@ private fun Bundle.putParam(param: Param) { fun VerificationMethod.toClientMethod(): Int { return when (this) { - VerificationMethod.TS43 -> 9 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 + } +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index afdb361a67..a1901667f6 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -257,7 +257,7 @@ private suspend fun runVerificationFlow( includeClientAuth = ConstellationStateStore.isPublicKeyAcked(context), callingPackage = callingPackage ) - val (verifications, isPublicKeyAcked) = executeSyncFlow( + val verifications = executeSyncFlow( context, sessionId, request, @@ -265,9 +265,6 @@ private suspend fun runVerificationFlow( buildContext, imsiToInfoMap ) - if (isPublicKeyAcked) { - ConstellationStateStore.setPublicKeyAcked(context, true) - } if (legacyCallback) { callbacks.onPhoneNumberVerified( @@ -285,7 +282,10 @@ private suspend fun runVerificationFlow( } private fun PhoneNumberVerification.toLegacyPhoneNumberInfoOrNull(): PhoneNumberInfo? { - if (verificationStatus != Verification.Status.STATUS_VERIFIED.value || phoneNumber.isNullOrEmpty()) { + if ( + verificationStatus != Verification.Status.STATUS_VERIFIED.toClientStatus() || + phoneNumber.isNullOrEmpty() + ) { return null } val extras = Bundle(this.extras ?: Bundle.EMPTY).apply { @@ -301,7 +301,7 @@ private suspend fun executeSyncFlow( syncRequest: SyncRequest, buildContext: RequestBuildContext, imsiToInfoMap: Map -): Pair, Boolean> = withContext(Dispatchers.IO) { +): Array = withContext(Dispatchers.IO) { Log.d(TAG, "Sending Sync request") val syncResponse = try { RpcClient.phoneDeviceVerificationClient.Sync().execute(syncRequest) @@ -335,7 +335,7 @@ private suspend fun executeSyncFlow( return@mapNotNull null } - val finalVerification = if (verification.getState() == Verification.State.PENDING) { + val finalVerification = if (verification.state == Verification.State.PENDING) { ChallengeProcessor.process( context, sessionId, @@ -347,12 +347,23 @@ private suspend fun executeSyncFlow( 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 to isPublicKeyAcked + verifications } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index 61105217b2..532c5733a4 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -11,7 +11,6 @@ import com.squareup.wire.GrpcException import com.squareup.wire.GrpcStatus import org.microg.gms.constellation.core.ConstellationStateStore import org.microg.gms.constellation.core.RpcClient -import org.microg.gms.constellation.core.getState import org.microg.gms.constellation.core.proto.ChallengeResponse import org.microg.gms.constellation.core.proto.ProceedRequest import org.microg.gms.constellation.core.proto.RequestHeader @@ -20,6 +19,7 @@ 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" @@ -42,10 +42,10 @@ object ChallengeProcessor { var currentVerification = verification for (attempt in 1..MAX_PROCEED_ROUNDS) { - if (currentVerification.getState() != Verification.State.PENDING) { + if (currentVerification.state != Verification.State.PENDING) { Log.d( TAG, - "Verification state: ${currentVerification.getState()}. Stopping sequential verification." + "Verification state: ${currentVerification.state}. Stopping sequential verification." ) return currentVerification } diff --git a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java index 8c2327f091..a0ced6ac30 100644 --- a/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java +++ b/play-services-constellation/src/main/java/com/google/android/gms/constellation/VerifyPhoneNumberRequest.java @@ -32,7 +32,7 @@ public class VerifyPhoneNumberRequest extends AbstractSafeParcelable { public final List targetedSims; @Field(6) - public final boolean silent; + public final boolean includeUnverified; @Field(7) public final int apiVersion; @@ -47,7 +47,7 @@ public VerifyPhoneNumberRequest( @Param(3) IdTokenRequest idTokenRequest, @Param(4) Bundle extras, @Param(5) List targetedSims, - @Param(6) boolean silent, + @Param(6) boolean includeUnverified, @Param(7) int apiVersion, @Param(8) List verificationMethodsValues ) { @@ -56,7 +56,7 @@ public VerifyPhoneNumberRequest( this.idTokenRequest = idTokenRequest; this.extras = extras; this.targetedSims = targetedSims; - this.silent = silent; + this.includeUnverified = includeUnverified; this.apiVersion = apiVersion; this.verificationMethodsValues = verificationMethodsValues; } From 58732b56d982785798e1b4d8257c44f542da5fb7 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 3 Apr 2026 04:15:32 -0400 Subject: [PATCH 20/63] Add more infos to `TelephonyInfo` --- .../core/src/main/AndroidManifest.xml | 1 + .../proto/builder/TelephonyInfoBuilder.kt | 86 ++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml index aa877ea5e5..db636fcc6e 100644 --- a/play-services-constellation/core/src/main/AndroidManifest.xml +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/TelephonyInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/TelephonyInfoBuilder.kt index 307ae6a24e..5bc26d6157 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/TelephonyInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/TelephonyInfoBuilder.kt @@ -4,15 +4,20 @@ 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 @@ -25,6 +30,7 @@ private const val TAG = "TelephonyInfoBuilder" @SuppressLint("MissingPermission") operator fun TelephonyInfo.Companion.invoke(context: Context, subscriptionId: Int): TelephonyInfo { val tm = context.getSystemService() + val subscriptionManager = context.getSystemService() val targetTm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && subscriptionId >= 0) { tm?.createForSubscriptionId(subscriptionId) } else tm @@ -67,6 +73,8 @@ operator fun TelephonyInfo.Companion.invoke(context: Context, subscriptionId: In operator_name = targetTm?.networkOperatorName ?: "" ) + val (activeSubCount, activeSubCountMax) = getActiveSubCounts(subscriptionManager) + return TelephonyInfo( phone_type = phoneType, group_id_level1 = targetTm?.groupIdLevel1 ?: "", @@ -74,12 +82,88 @@ operator fun TelephonyInfo.Companion.invoke(context: Context, subscriptionId: In network_info = networkInfo, network_roaming = networkRoaming, connectivity_state = connectivityState, - sms_capability = if (targetTm?.isSmsCapable == true) TelephonyInfo.SmsCapability.SMS_CAPABLE else TelephonyInfo.SmsCapability.SMS_NOT_CAPABLE, + 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()?.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 { + 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 { val connectivityInfos = mutableListOf() try { From edb6de4ec6fb01351fe3e180232ca3a46792de73 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 3 Apr 2026 04:49:13 -0400 Subject: [PATCH 21/63] Improve DroidGuard generation --- .../core/proto/builder/ClientInfoBuilder.kt | 36 ++++++++++--------- .../core/proto/builder/CommonBuilders.kt | 5 +-- .../core/proto/builder/SyncRequestBuilder.kt | 9 ++++- .../core/verification/ChallengeProcessor.kt | 3 +- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt index e3265a57b1..ae9d3a9e9a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt @@ -7,6 +7,7 @@ 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.edit @@ -24,30 +25,20 @@ 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.GaiaToken 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, iidToken: String): ClientInfo { - return ClientInfo( - context, - RequestBuildContext( - iidToken = iidToken, - gaiaTokens = GaiaToken.getList(context) - ) - ) -} - @SuppressLint("HardwareIds") suspend operator fun ClientInfo.Companion.invoke( context: Context, - buildContext: RequestBuildContext + buildContext: RequestBuildContext, + rpc: String ): ClientInfo { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val locale = Locale.getDefault().let { "${it.language}_${it.country}" } @@ -70,7 +61,7 @@ suspend operator fun ClientInfo.Companion.invoke( is_wearable_standalone = false, gaia_signals = GaiaSignals(context), device_fingerprint = Build.FINGERPRINT, - droidguard_signals = DroidGuardSignals(context), + droidguard_signals = DroidGuardSignals(context, buildContext.iidToken, rpc), ) } @@ -151,17 +142,28 @@ operator fun CountryInfo.Companion.invoke(context: Context): CountryInfo { return CountryInfo(sim_countries = simCountries, network_countries = networkCountries) } -suspend operator fun DroidGuardSignals.Companion.invoke(context: Context): DroidGuardSignals? { +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( - "package_name" to Constants.GMS_PACKAGE_NAME, - "timestamp" to System.currentTimeMillis().toString() + "iidHash" to iidHash, + "rpc" to rpc ) val result = client.getResults("constellation_verify", data, null).await() DroidGuardSignals(droidguard_result = result, droidguard_token = "") diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt index b34b11010f..8cf436b2b6 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt @@ -47,7 +47,8 @@ suspend operator fun RequestHeader.Companion.invoke( sessionId: String, buildContext: RequestBuildContext, triggerType: RequestTrigger.Type = RequestTrigger.Type.CONSENT_API_TRIGGER, - includeClientAuth: Boolean = false + includeClientAuth: Boolean = false, + rpc: String ): RequestHeader { val authManager = if (includeClientAuth) context.authManager else null val clientAuth = if (includeClientAuth) { @@ -62,7 +63,7 @@ suspend operator fun RequestHeader.Companion.invoke( } return RequestHeader( - client_info = ClientInfo(context, buildContext), + client_info = ClientInfo(context, buildContext, rpc), client_auth = clientAuth, session_id = sessionId, trigger = RequestTrigger(type = triggerType) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt index d58eaeb8f7..27eb45e747 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt @@ -192,7 +192,14 @@ suspend operator fun SyncRequest.Companion.invoke( return SyncRequest( verifications = verifications, - header_ = RequestHeader(context, sessionId, buildContext, triggerType, includeClientAuth), + header_ = RequestHeader( + context, + sessionId, + buildContext, + triggerType, + includeClientAuth, + "sync" + ), verification_tokens = ConstellationStateStore.loadVerificationTokens(context) ) } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index 532c5733a4..eb708cfd8d 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -107,7 +107,8 @@ object ChallengeProcessor { sessionId, buildContext, RequestTrigger.Type.TRIGGER_API_CALL, - includeClientAuth = true + includeClientAuth = true, + "proceed" ) val proceedRequest = ProceedRequest( verification = currentVerification, From 94c442ae107d060d03779f4ed0cf23343e05d2c6 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:55:51 -0400 Subject: [PATCH 22/63] Improve `MtSmsVerifier` --- .../core/src/main/AndroidManifest.xml | 3 + .../constellation/core/VerifyPhoneNumber.kt | 26 ++- .../core/verification/ChallengeProcessor.kt | 2 +- .../core/verification/MtSmsVerifier.kt | 201 ++++++++++++------ 4 files changed, 163 insertions(+), 69 deletions(-) diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml index db636fcc6e..943034c187 100644 --- a/play-services-constellation/core/src/main/AndroidManifest.xml +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -18,6 +18,9 @@ + diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index a1901667f6..2578baf068 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -28,6 +28,7 @@ import org.microg.gms.constellation.core.proto.builder.buildImsiToSubscriptionIn import org.microg.gms.constellation.core.proto.builder.buildRequestContext 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" @@ -257,14 +258,23 @@ private suspend fun runVerificationFlow( includeClientAuth = ConstellationStateStore.isPublicKeyAcked(context), callingPackage = callingPackage ) - val verifications = executeSyncFlow( - context, - sessionId, - request, - syncRequest, - buildContext, - imsiToInfoMap - ) + + MtSmsInboxRegistry.prepare( + context.applicationContext, + imsiToInfoMap.values.map { it.subscriptionId }) + + val verifications = try { + executeSyncFlow( + context, + sessionId, + request, + syncRequest, + buildContext, + imsiToInfoMap + ) + } finally { + MtSmsInboxRegistry.dispose() + } if (legacyCallback) { callbacks.onPhoneNumberVerified( diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index eb708cfd8d..17a2a0f37b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -85,7 +85,7 @@ object ChallengeProcessor { } } - VerificationMethod.MT_SMS -> challenge.mt_challenge?.verify(context, subId) + VerificationMethod.MT_SMS -> challenge.mt_challenge?.verify(subId, remainingMillis) VerificationMethod.MO_SMS -> challenge.mo_challenge?.verify(context, subId) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index 4ad7e84e6a..fe7abe47a1 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -6,88 +6,169 @@ import android.content.Intent import android.content.IntentFilter import android.os.Build import android.provider.Telephony +import android.telephony.SmsMessage import android.util.Log +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 kotlin.coroutines.resume private const val TAG = "MtSmsVerifier" -suspend fun MTChallenge.verify(context: Context, subId: Int): ChallengeResponse? { +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?.coerceIn(0L..300_000L) ?: 300_000L Log.d(TAG, "Waiting for MT SMS containing challenge string") - val result = withTimeoutOrNull(300_000) { - suspendCancellableCoroutine?> { continuation -> - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - try { - if (intent.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) { - val receivedSubId = intent.getIntExtra( - "android.telephony.extra.SUBSCRIPTION_INDEX", - intent.getIntExtra("subscription", -1) - ) - if (subId != -1 && receivedSubId != -1 && receivedSubId != subId) { - return - } - val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) - for (msg in messages) { - val body = msg.messageBody - if (body != null && body.contains(expectedBody)) { - Log.d( - TAG, - "Matching MT SMS received from ${msg.originatingAddress}" - ) - context.unregisterReceiver(this) - if (continuation.isActive) { - continuation.resume( - Pair( - body, - msg.originatingAddress ?: "" - ) - ) - } - return - } - } - } - } catch (e: Exception) { - Log.e(TAG, "Error in MT SMS receiver", e) + 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() + + fun prepare(context: Context, subIds: Iterable) { + 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 +) + +internal class MtSmsInbox( + context: Context, + private val subId: Int +) { + private val context = context.applicationContext + private val lock = Any() + private val bufferedMessages = mutableListOf() + private val pendingMatches = mutableListOf() + + 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) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + context.registerReceiver(receiver, filter) + } + } + + 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) } } } + } + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver( - receiver, - IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION), - Context.RECEIVER_NOT_EXPORTED - ) - } else { - context.registerReceiver( - receiver, - IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) - ) - } - continuation.invokeOnCancellation { - try { - context.unregisterReceiver(receiver) - } catch (_: Exception) { + private fun onMessagesReceived(messages: Array) { + 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) + } } } } } - return result?.let { (body, sender) -> - ChallengeResponse( - mt_response = MTChallengeResponseData( - sms = body, - sender = sender - ) - ) + fun dispose() { + synchronized(lock) { + pendingMatches.clear() + bufferedMessages.clear() + } + try { + context.unregisterReceiver(receiver) + } catch (_: IllegalArgumentException) { + } } } From eddc24dccd288b9b853a918818f66e03c2a48517 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:02:42 -0400 Subject: [PATCH 23/63] Reorder `RequestHeader` arguments --- .../gms/constellation/core/proto/builder/CommonBuilders.kt | 4 ++-- .../constellation/core/proto/builder/SyncRequestBuilder.kt | 2 +- .../gms/constellation/core/verification/ChallengeProcessor.kt | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt index 8cf436b2b6..f93d8f21ed 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/CommonBuilders.kt @@ -46,9 +46,9 @@ suspend operator fun RequestHeader.Companion.invoke( context: Context, sessionId: String, buildContext: RequestBuildContext, - triggerType: RequestTrigger.Type = RequestTrigger.Type.CONSENT_API_TRIGGER, + rpc: String, + triggerType: RequestTrigger.Type = RequestTrigger.Type.TRIGGER_API_CALL, includeClientAuth: Boolean = false, - rpc: String ): RequestHeader { val authManager = if (includeClientAuth) context.authManager else null val clientAuth = if (includeClientAuth) { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt index 27eb45e747..b68f0d5991 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt @@ -196,9 +196,9 @@ suspend operator fun SyncRequest.Companion.invoke( context, sessionId, buildContext, + "sync", triggerType, includeClientAuth, - "sync" ), verification_tokens = ConstellationStateStore.loadVerificationTokens(context) ) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index 17a2a0f37b..a5190b30eb 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -106,9 +106,8 @@ object ChallengeProcessor { context, sessionId, buildContext, - RequestTrigger.Type.TRIGGER_API_CALL, + "proceed", includeClientAuth = true, - "proceed" ) val proceedRequest = ProceedRequest( verification = currentVerification, From 85567bae3c97413d2cbf6fc4b01771bc5c4d98ac Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 4 Apr 2026 02:00:25 -0400 Subject: [PATCH 24/63] Improve RPC error handling --- .../constellation/core/VerifyPhoneNumber.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 2578baf068..af7ae4358f 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -202,10 +202,15 @@ private suspend fun handleVerifyPhoneNumberRequest( } } catch (e: Exception) { Log.e(TAG, "verifyPhoneNumber failed", e) + val status = if (readCallbackMode == ReadCallbackMode.NONE && e is GrpcException) { + handleRpcError(e) + } else { + Status.INTERNAL_ERROR + } when (readCallbackMode) { ReadCallbackMode.LEGACY -> { callbacks.onPhoneNumberVerified( - Status.INTERNAL_ERROR, + status, emptyList(), ApiMetadata.DEFAULT ) @@ -214,13 +219,13 @@ private suspend fun handleVerifyPhoneNumberRequest( ReadCallbackMode.NONE -> { if (legacyCallbackOnFullFlow) { callbacks.onPhoneNumberVerified( - Status.INTERNAL_ERROR, + status, emptyList(), ApiMetadata.DEFAULT ) } else { callbacks.onPhoneNumberVerificationsCompleted( - Status.INTERNAL_ERROR, + status, VerifyPhoneNumberResponse(emptyArray(), Bundle()), ApiMetadata.DEFAULT ) @@ -229,7 +234,7 @@ private suspend fun handleVerifyPhoneNumberRequest( ReadCallbackMode.TYPED -> { callbacks.onPhoneNumberVerificationsCompleted( - Status.INTERNAL_ERROR, + status, VerifyPhoneNumberResponse(emptyArray(), Bundle()), ApiMetadata.DEFAULT ) @@ -238,6 +243,19 @@ private suspend fun handleVerifyPhoneNumberRequest( } } +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 suspend fun runVerificationFlow( context: Context, request: VerifyPhoneNumberRequest, From f3449a71f615b7044a6b6f9165615e1059c7e770 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 4 Apr 2026 04:42:26 -0400 Subject: [PATCH 25/63] Improve `MoSmsVerifier` --- .../core/verification/ChallengeProcessor.kt | 26 ++++++- .../core/verification/MoSmsVerifier.kt | 77 +++++++++++++++---- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index a5190b30eb..c09e26022b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -9,12 +9,12 @@ 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.RequestTrigger 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 @@ -40,6 +40,7 @@ object ChallengeProcessor { verification: Verification, ): Verification { var currentVerification = verification + var moSession: MoSmsSession? = null for (attempt in 1..MAX_PROCEED_ROUNDS) { if (currentVerification.state != Verification.State.PENDING) { @@ -73,6 +74,9 @@ object ChallengeProcessor { 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 + } val challengeResponse: ChallengeResponse? = when (challenge.type) { VerificationMethod.TS43 -> challenge.ts43_challenge?.verify(context, subId) @@ -87,7 +91,24 @@ object ChallengeProcessor { VerificationMethod.MT_SMS -> challenge.mt_challenge?.verify(subId, remainingMillis) - VerificationMethod.MO_SMS -> challenge.mo_challenge?.verify(context, subId) + VerificationMethod.MO_SMS -> challenge.mo_challenge?.let { moChallenge -> + val activeSession = moSession?.takeIf { it.matches(challengeId, subId) } + if (activeSession != null) { + val delayMillis = activeSession.nextPollingDelayMillis(remainingMillis) + if (delayMillis > 0L) { + Log.d( + TAG, + "Attempt $attempt: Challenge $challengeId still pending. Waiting ${delayMillis}ms before next MO proceed ping." + ) + delay(delayMillis) + } + activeSession.response + } else { + moChallenge.startSession(context, challengeId, subId).also { + moSession = it + }.response + } + } VerificationMethod.REGISTERED_SMS -> challenge.registered_sms_challenge?.verify( context, @@ -95,6 +116,7 @@ object ChallengeProcessor { ) else -> { + moSession = null Log.w(TAG, "Unsupported verification method: ${challenge.type}") null } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index 2ab7176df7..e77b97fc82 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -26,14 +26,54 @@ 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" -suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse { +data class MoSmsSession( + val challengeId: String, + val subId: Int, + val response: ChallengeResponse, + private val pollingIntervalsMillis: List, + 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 ChallengeResponse(mo_response = MOChallengeResponseData(status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO)) + return failedMoResponse() } if (context.checkCallingOrSelfPermission(Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) { Log.w(TAG, "SEND_SMS permission missing") - return ChallengeResponse(mo_response = MOChallengeResponseData(status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO)) + return failedMoResponse() } val smsManager = resolveSmsManager(context, subId) ?: return ChallengeResponse( @@ -64,7 +104,7 @@ suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse Log.d(TAG, "Sending MO SMS to $proxy_number with messageId: $messageId") - return withTimeoutOrNull(30000) { + return withTimeoutOrNull(30_000L) { suspendCancellableCoroutine { continuation -> val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -150,17 +190,28 @@ suspend fun MoChallenge.verify(context: Context, subId: Int): ChallengeResponse } catch (_: Exception) { } if (continuation.isActive) { - continuation.resume( - ChallengeResponse( - mo_response = MOChallengeResponseData( - status = MOChallengeResponseData.Status.FAILED_TO_SEND_MO - ) - ) - ) + continuation.resume(failedMoResponse()) } } } - } ?: ChallengeResponse( + } ?: failedMoResponse() +} + +private fun MoChallenge.pollingIntervalsMillis(): List { + val configuredIntervals = polling_intervals.parsePollingIntervals() + return configuredIntervals.ifEmpty { + DEFAULT_MO_POLLING_INTERVALS_MILLIS.parsePollingIntervals() + } +} + +private fun String.parsePollingIntervals(): List { + 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 ) @@ -218,4 +269,4 @@ private fun resolveSmsManager(context: Context, subId: Int): SmsManager? { Log.e(TAG, "Failed to resolve SmsManager for subId: $subId", e) null } -} \ No newline at end of file +} From 6920424d6e7375529204d2af8d4604a9ca996ad1 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:18:12 -0400 Subject: [PATCH 26/63] Improve MT SMS timeout behavior --- .../core/verification/ChallengeProcessor.kt | 11 +++++++++-- .../constellation/core/verification/MtSmsVerifier.kt | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index c09e26022b..54427076e7 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -63,8 +63,15 @@ object ChallengeProcessor { val challengeId = challenge.challenge_id?.id ?: "" val remainingMillis = challengeTimeRemainingMillis(currentVerification) if (remainingMillis != null && remainingMillis <= 0L) { - Log.w(TAG, "Attempt $attempt: Challenge $challengeId expired before proceed") - return currentVerification + 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, diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index fe7abe47a1..fb7447ec7a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -15,6 +15,7 @@ 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" @@ -25,7 +26,7 @@ suspend fun MTChallenge.verify( ): ChallengeResponse? { val expectedBody = sms.takeIf { it.isNotEmpty() } ?: return null val inbox = MtSmsInboxRegistry.get(subId) - val effectiveTimeoutMillis = timeoutMillis?.coerceIn(0L..300_000L) ?: 300_000L + val effectiveTimeoutMillis = timeoutMillis?.coerceAtLeast(0L) ?: TimeUnit.MINUTES.toMillis(30) Log.d(TAG, "Waiting for MT SMS containing challenge string") From fbcf53c57de7906e31b943aa0927923451366358 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:19:10 -0400 Subject: [PATCH 27/63] Add consent check before verification --- .../constellation/core/VerifyPhoneNumber.kt | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index af7ae4358f..848978f6c9 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -21,6 +21,12 @@ 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.DeviceID +import org.microg.gms.constellation.core.proto.GetConsentRequest +import org.microg.gms.constellation.core.proto.GetConsentResponse +import org.microg.gms.constellation.core.proto.RequestHeader 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 @@ -202,10 +208,11 @@ private suspend fun handleVerifyPhoneNumberRequest( } } catch (e: Exception) { Log.e(TAG, "verifyPhoneNumber failed", e) - val status = if (readCallbackMode == ReadCallbackMode.NONE && e is GrpcException) { - handleRpcError(e) - } else { - Status.INTERNAL_ERROR + val status = when { + readCallbackMode != ReadCallbackMode.NONE -> Status.INTERNAL_ERROR + e is GrpcException -> handleRpcError(e) + e is NoConsentException -> Status(5001) + else -> Status.INTERNAL_ERROR } when (readCallbackMode) { ReadCallbackMode.LEGACY -> { @@ -256,6 +263,8 @@ private fun handleRpcError(error: GrpcException): Status { return Status(statusCode, error.message) } +private class NoConsentException : Exception("No consent") + private suspend fun runVerificationFlow( context: Context, request: VerifyPhoneNumberRequest, @@ -267,6 +276,34 @@ private suspend fun runVerificationFlow( 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. Not running verification.") + throw NoConsentException() + } + } + val syncRequest = SyncRequest( context, sessionId, @@ -395,3 +432,36 @@ private suspend fun executeSyncFlow( 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 + } +} \ No newline at end of file From 9904b50c4ea02b7d799ae225435b3d75cac9ef6f Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:24:14 -0400 Subject: [PATCH 28/63] Improve `CarrierIdVerifier` --- .../core/verification/CarrierIdVerifier.kt | 48 +++++++++++++------ .../core/verification/ChallengeProcessor.kt | 46 +++++++++++++++++- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt index 00d0fb8919..05091a9f7c 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt @@ -15,6 +15,16 @@ 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, @@ -30,22 +40,25 @@ fun Challenge.verifyCarrierId(context: Context, subId: Int): ChallengeResponse { "No active subscription for carrier auth" ) - val appType = carrierChallenge.app_type - val authType = carrierChallenge.auth_type + val telephonyManager = context.getSystemService() + ?: 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" + ) + } - return try { - val telephonyManager = context.getSystemService() - ?: return failure( - CarrierIdError.CARRIER_ID_ERROR_NOT_SUPPORTED, - "TelephonyManager unavailable" - ) - val targetManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - telephonyManager.createForSubscriptionId(subId) - } else { - telephonyManager - } + val appType = carrierChallenge.app_type.takeIf { it != 0 } ?: TelephonyManager.APPTYPE_USIM - val response = targetManager.getIccAuthentication(appType, authType, challengeData) + 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 { @@ -77,6 +90,13 @@ fun Challenge.verifyCarrierId(context: Context, subId: Int): ChallengeResponse { } } +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( diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index 54427076e7..e121a44dfe 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -41,6 +41,7 @@ object ChallengeProcessor { ): 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) { @@ -73,6 +74,7 @@ object ChallengeProcessor { return currentVerification } } + Log.d( TAG, "Attempt $attempt: Solving challenge ID: $challengeId, Type: ${challenge.type}" @@ -81,9 +83,13 @@ object ChallengeProcessor { 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 = null + } val challengeResponse: ChallengeResponse? = when (challenge.type) { VerificationMethod.TS43 -> challenge.ts43_challenge?.verify(context, subId) @@ -92,7 +98,31 @@ object ChallengeProcessor { if (challenge.ts43_challenge != null) { challenge.ts43_challenge.verify(context, subId) } else { - challenge.verifyCarrierId(context, subId) + val activeSession = + carrierIdSession?.takeIf { it.matches(challengeId, subId) } + ?: CarrierIdSession(challengeId, subId).also { + carrierIdSession = it + } + + activeSession.attempts += 1 + + if (activeSession.attempts >= 4) { + Log.w( + TAG, + "Attempt $attempt: Carrier ID challenge $challengeId is still pending after retry-exceeded response. Stopping." + ) + return currentVerification + } + + if (activeSession.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) + } } } @@ -159,6 +189,18 @@ object ChallengeProcessor { } ConstellationStateStore.storeProceedResponse(context, proceedResponse) currentVerification = proceedResponse.verification ?: currentVerification + + if (challenge.type == VerificationMethod.CARRIER_ID) { + 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 + } + } } else { Log.w( TAG, @@ -172,4 +214,4 @@ object ChallengeProcessor { Log.w(TAG, "Exhausted all $MAX_PROCEED_ROUNDS proceed rounds, record is still pending.") return currentVerification } -} +} \ No newline at end of file From ec02375745193d2b26be61083577d460cb68cc30 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:12:16 -0400 Subject: [PATCH 29/63] Update `constellation.proto` --- .../core/ConstellationStateStore.kt | 4 +- .../core/src/main/proto/constellation.proto | 40 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt index f41739c10b..ef2ebe25d1 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt @@ -10,9 +10,9 @@ 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.ConsentVersion import org.microg.gms.constellation.core.proto.DroidguardToken import org.microg.gms.constellation.core.proto.ProceedResponse import org.microg.gms.constellation.core.proto.ServerTimestamp @@ -132,7 +132,7 @@ object ConstellationStateStore { context: Context, consent: Consent, source: ConsentSource, - version: ConsentVersion + version: DeviceConsentVersion ) { statePrefs(context).edit { putInt(KEY_PNVR_NOTICE_CONSENT, consent.value) diff --git a/play-services-constellation/core/src/main/proto/constellation.proto b/play-services-constellation/core/src/main/proto/constellation.proto index de4e950b6e..98297b63b2 100644 --- a/play-services-constellation/core/src/main/proto/constellation.proto +++ b/play-services-constellation/core/src/main/proto/constellation.proto @@ -755,15 +755,13 @@ message GetConsentResponse { PermissionState permission_state = 10; } -enum FlowContext { - option allow_alias = true; - FLOW_CONTEXT_UNSPECIFIED = 0; - FLOW_CONTEXT_RCS_DEFAULT_ON_LEGAL_FYI = 0; - FLOW_CONTEXT_RCS_CONSENT = 1; - FLOW_CONTEXT_RCS_DEFAULT_ON_OUT_OF_BOX = 2; - FLOW_CONTEXT_RCS_SAMSUNG_UNFREEZE = 3; - FLOW_CONTEXT_RCS_DEFAULT_ON_LEGAL_FYI_IN_SETTINGS = 4; - FLOW_CONTEXT_RCS_UNKNOWN_5 = 5; +enum ConsentVersion { + CONSENT_VERSION_UNSPECIFIED = 0; + RCS_CONSENT = 1; + RCS_DEFAULT_ON_LEGAL_FYI = 2; + RCS_DEFAULT_ON_OUT_OF_BOX = 3; + RCS_SAMSUNG_UNFREEZE = 4; + RCS_DEFAULT_ON_LEGAL_FYI_IN_SETTINGS = 5; } message SetConsentRequest { @@ -773,7 +771,7 @@ message SetConsentRequest { bytes audit_record = 6; repeated Param api_params = 7; OnDemandConsent on_demand_consent = 8; - FlowContext flow_context = 9; + ConsentVersion consent_version = 9; oneof device_consent_oneof { AsterismConsent device_consent = 10; } @@ -789,22 +787,18 @@ enum Consent { EXPIRED = 3; } -enum ConsentVersion { - option allow_alias = true; - CONSENT_VERSION_UNSPECIFIED = 0; - RCS_LEGAL_FYI = 0; - RCS_CONSENT = 1; - RCS_OUT_OF_BOX = 2; - PHONE_VERIFICATION_DEFAULT = 1; - PHONE_VERIFICATION_MESSAGES_CALLS_V1 = 2; - PHONE_VERIFICATION_INTL_SMS_CALLS = 3; - PHONE_VERIFICATION_REACHABILITY_INTL_SMS_CALLS = 4; -} - message AsterismConsent { Consent consent = 1; ConsentSource consent_source = 2; - ConsentVersion consent_version = 3; + DeviceConsentVersion consent_version = 3; + + enum DeviceConsentVersion { + UNKNOWN = 0; + PHONE_VERIFICATION_DEFAULT = 1; + PHONE_VERIFICATION_MESSAGES_CALLS_V1 = 2; + PHONE_VERIFICATION_INTL_SMS_CALLS = 3; + PHONE_VERIFICATION_REACHABILITY_INTL_SMS_CALLS = 4; + } } message ConsentState { From ce85aeb232f9f9d49e3ec23e98407ccd130c3a41 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:30:18 -0400 Subject: [PATCH 30/63] Small cleanups --- .../core/proto/builder/ClientInfoBuilder.kt | 8 ++++--- .../core/verification/CarrierIdVerifier.kt | 3 ++- .../core/verification/MoSmsVerifier.kt | 22 ++++++------------- .../core/verification/MtSmsVerifier.kt | 13 ++++++----- 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt index ae9d3a9e9a..b1644dc893 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/ClientInfoBuilder.kt @@ -5,6 +5,8 @@ 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 @@ -83,15 +85,15 @@ operator fun DeviceID.Companion.invoke(context: Context, iidToken: String): Devi } val userSerial = try { - val userManager = context.getSystemService() - userManager?.getSerialNumberForUser(android.os.Process.myUserHandle()) ?: 0L + val userManager = context.getSystemService() + userManager?.getSerialNumberForUser(Process.myUserHandle()) ?: 0L } catch (_: Exception) { 0L } var primaryDeviceId = prefs.getLong("primary_device_id", 0L) val isSystemUser = try { - val userManager = context.getSystemService() + val userManager = context.getSystemService() userManager?.isSystemUser ?: true } catch (_: Exception) { true diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt index 05091a9f7c..42d73ac890 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/CarrierIdVerifier.kt @@ -58,7 +58,8 @@ fun Challenge.verifyCarrierId(context: Context, subId: Int): ChallengeResponse { val appType = carrierChallenge.app_type.takeIf { it != 0 } ?: TelephonyManager.APPTYPE_USIM return try { - val response = targetManager.getIccAuthentication(appType, carrierChallenge.auth_type, challengeData) + val response = + targetManager.getIccAuthentication(appType, carrierChallenge.auth_type, challengeData) if (response.isNullOrEmpty()) { failure(CarrierIdError.CARRIER_ID_ERROR_NULL_RESPONSE, "Null ISIM response") } else { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index e77b97fc82..458712c4cb 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -142,20 +142,12 @@ private suspend fun MoChallenge.sendOnce(context: Context, subId: Int): Challeng } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver( - receiver, - IntentFilter(action), - Context.RECEIVER_NOT_EXPORTED - ) - } else { - ContextCompat.registerReceiver( - context, - receiver, - IntentFilter(action), - ContextCompat.RECEIVER_NOT_EXPORTED - ) - } + ContextCompat.registerReceiver( + context, + receiver, + IntentFilter(action), + ContextCompat.RECEIVER_NOT_EXPORTED + ) continuation.invokeOnCancellation { try { @@ -252,7 +244,7 @@ private fun resolveSmsManager(context: Context, subId: Int): SmsManager? { return try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val manager = context.getSystemService(SmsManager::class.java) + val manager = context.getSystemService() if (subId != -1) { manager?.createForSubscriptionId(subId) } else { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index fb7447ec7a..7a522c1e18 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -4,10 +4,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.Build 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 @@ -108,11 +108,12 @@ internal class MtSmsInbox( init { val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) - } else { - context.registerReceiver(receiver, filter) - } + ContextCompat.registerReceiver( + context, + receiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED + ) } suspend fun awaitMatch(expectedBody: String): ReceivedSms? { From bafa5f405a1efe4a0e07bb74d66289ab661fc768 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:39:20 -0400 Subject: [PATCH 31/63] Add permissions to Self-Check --- .../main/java/org/microg/gms/ui/SelfCheckFragment.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java b/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java index 8d809ca0a7..789db690b9 100644 --- a/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java @@ -50,8 +50,11 @@ import static android.Manifest.permission.GET_ACCOUNTS; import static android.Manifest.permission.POST_NOTIFICATIONS; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.Manifest.permission.READ_PHONE_NUMBERS; import static android.Manifest.permission.READ_PHONE_STATE; +import static android.Manifest.permission.READ_SMS; import static android.Manifest.permission.RECEIVE_SMS; +import static android.Manifest.permission.SEND_SMS; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.os.Build.VERSION.SDK_INT; @@ -76,8 +79,13 @@ protected void prepareSelfCheckList(Context context, List checks if (SDK_INT >= 33) { permissions.add(POST_NOTIFICATIONS); } + if (SDK_INT >= 26) { + permissions.add(READ_PHONE_NUMBERS); + } permissions.add(READ_PHONE_STATE); + permissions.add(READ_SMS); permissions.add(RECEIVE_SMS); + permissions.add(SEND_SMS); checks.add(new PermissionCheckGroup(permissions.toArray(new String[0])) { @Override public void doChecks(Context context, ResultCollector collector) { From a1372b231d20e7e1331b4c502b4081ffe9cf2728 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:52:57 -0400 Subject: [PATCH 32/63] Adjust RPC timeout --- .../main/kotlin/org/microg/gms/constellation/core/RpcClient.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt index e5030bac66..1ab73d8d93 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt @@ -1,13 +1,16 @@ package org.microg.gms.constellation.core +import com.squareup.wire.Duration 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() + .callTimeout(60, TimeUnit.SECONDS) .addInterceptor { chain -> val originalRequest = chain.request() val builder = originalRequest.newBuilder() From 564ad088f07e24ea182a82b2bdd04cb02edc8bdd Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:34:14 -0400 Subject: [PATCH 33/63] `RpcClient` formatting --- .../kotlin/org/microg/gms/constellation/core/RpcClient.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt index 1ab73d8d93..c4a53de4f3 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt @@ -29,8 +29,8 @@ object RpcClient { .build() val phoneDeviceVerificationClient: PhoneDeviceVerificationClient = - grpcClient.create(PhoneDeviceVerificationClient::class) + grpcClient.create() val phoneNumberClient: PhoneNumberClient = - grpcClient.create(PhoneNumberClient::class) + grpcClient.create() } From 2ec6d36ecb69564211f8c6aa0356abfbc751f512 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:07:28 -0400 Subject: [PATCH 34/63] Wrap callbacks in `try catch` --- .../core/ConstellationApiService.kt | 43 +++++++++++-- .../core/ConstellationCallbacksWrapper.kt | 63 +++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationCallbacksWrapper.kt diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt index 39334ec569..e0c76a4b42 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationApiService.kt @@ -78,7 +78,14 @@ class ConstellationApiServiceImpl( ) if (cb == null || bundle == null) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - serviceScope.launch { handleVerifyPhoneNumberV1(context, cb, bundle, packageName) } + serviceScope.launch { + handleVerifyPhoneNumberV1( + context, + ConstellationCallbacksWrapper(cb), + bundle, + packageName + ) + } } override fun verifyPhoneNumberSingleUse( @@ -89,7 +96,14 @@ class ConstellationApiServiceImpl( Log.i(TAG, "verifyPhoneNumberSingleUse()") if (cb == null || bundle == null) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - serviceScope.launch { handleVerifyPhoneNumberSingleUse(context, cb, bundle, packageName) } + serviceScope.launch { + handleVerifyPhoneNumberSingleUse( + context, + ConstellationCallbacksWrapper(cb), + bundle, + packageName + ) + } } override fun verifyPhoneNumber( @@ -103,7 +117,14 @@ class ConstellationApiServiceImpl( ) if (cb == null || request == null) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - serviceScope.launch { handleVerifyPhoneNumberRequest(context, cb, request, packageName) } + serviceScope.launch { + handleVerifyPhoneNumberRequest( + context, + ConstellationCallbacksWrapper(cb), + request, + packageName + ) + } } override fun getIidToken( @@ -113,7 +134,13 @@ class ConstellationApiServiceImpl( ) { Log.i(TAG, "getIidToken(): $request") if (cb == null || request == null) return - serviceScope.launch { handleGetIidToken(context, cb, request) } + serviceScope.launch { + handleGetIidToken( + context, + ConstellationCallbacksWrapper(cb), + request + ) + } } override fun getPnvCapabilities( @@ -124,6 +151,12 @@ class ConstellationApiServiceImpl( 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, cb, request) } + serviceScope.launch { + handleGetPnvCapabilities( + context, + ConstellationCallbacksWrapper(cb), + request + ) + } } } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationCallbacksWrapper.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationCallbacksWrapper.kt new file mode 100644 index 0000000000..8fa0aa61f1 --- /dev/null +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationCallbacksWrapper.kt @@ -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?, + 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" + } +} \ No newline at end of file From 2b8cf43824339d4346b84e197a1baeaa5c5e45e1 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Wed, 8 Apr 2026 05:32:10 -0400 Subject: [PATCH 35/63] Refactoring --- .../core/ConstellationStateStore.kt | 32 +- .../core/GetVerifiedPhoneNumbers.kt | 35 +- .../gms/constellation/core/RpcClient.kt | 1 - .../constellation/core/VerifyPhoneNumber.kt | 101 +-- .../core/verification/ChallengeProcessor.kt | 155 +++-- .../core/verification/MoSmsVerifier.kt | 105 ++- .../verification/RegisteredSmsVerifier.kt | 3 +- .../core/verification/Ts43Verifier.kt | 598 +++++++++--------- 8 files changed, 467 insertions(+), 563 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt index ef2ebe25d1..8f581309d2 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt @@ -90,18 +90,6 @@ object ConstellationStateStore { return null } - private fun storeCachedDroidGuardToken( - context: Context, - token: String, - expirationMillis: Long - ) { - if (token.isBlank() || expirationMillis <= System.currentTimeMillis()) return - statePrefs(context).edit { - putString(KEY_DROIDGUARD_TOKEN, token) - putLong(KEY_DROIDGUARD_TOKEN_TTL, expirationMillis) - } - } - fun clearDroidGuardToken(context: Context) { statePrefs(context).edit { remove(KEY_DROIDGUARD_TOKEN) @@ -160,23 +148,25 @@ object ConstellationStateStore { private fun storeDroidGuardToken(context: Context, token: DroidguardToken?) { val tokenValue = token?.token?.takeIf { it.isNotEmpty() } ?: return val expiration = token.ttl?.toEpochMilli() ?: return - storeCachedDroidGuardToken(context, tokenValue, expiration) + 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 nextSyncDelayMillis = timestamp?.let(::nextSyncDelayMillis) ?: return + 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() + nextSyncDelayMillis) + putLong( + KEY_NEXT_SYNC_TIMESTAMP_MS, + System.currentTimeMillis() + serverMillis - localMillis + ) } } - private fun nextSyncDelayMillis(timestamp: ServerTimestamp): Long { - val serverMillis = timestamp.timestamp?.toEpochMilli() ?: 0L - val localMillis = timestamp.now?.toEpochMilli() ?: 0L - return serverMillis - localMillis - } - private fun statePrefs(context: Context) = context.getSharedPreferences(STATE_PREFS_NAME, Context.MODE_PRIVATE) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index 5f73229910..cd63473dd9 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -4,15 +4,10 @@ package org.microg.gms.constellation.core import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle 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.PhoneNumberInfo -import com.google.android.gms.constellation.VerifyPhoneNumberResponse import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification -import com.google.android.gms.constellation.internal.IConstellationCallbacks import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString @@ -26,25 +21,6 @@ import java.util.UUID private const val TAG = "GetVerifiedPhoneNumbers" -suspend fun handleGetVerifiedPhoneNumbers( - context: Context, - callbacks: IConstellationCallbacks, - bundle: Bundle -) = withContext(Dispatchers.IO) { - try { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - throw Exception("Unsupported SDK") - } - - val phoneNumbers = fetchVerifiedPhoneNumbers(context, bundle).map { it.toPhoneNumberInfo() } - - callbacks.onPhoneNumberVerified(Status.SUCCESS, phoneNumbers, ApiMetadata.DEFAULT) - } catch (e: Exception) { - Log.e(TAG, "Error in GetVerifiedPhoneNumbers (read-only)", e) - callbacks.onPhoneNumberVerified(Status.INTERNAL_ERROR, emptyList(), ApiMetadata.DEFAULT) - } -} - internal suspend fun fetchVerifiedPhoneNumbers( context: Context, bundle: Bundle, @@ -87,14 +63,7 @@ internal suspend fun fetchVerifiedPhoneNumbers( response.phone_numbers } -internal fun List.toVerifyPhoneNumberResponse(): VerifyPhoneNumberResponse { - return VerifyPhoneNumberResponse( - map { it.toPhoneNumberVerification() }.toTypedArray(), - Bundle.EMPTY - ) -} - -private fun VerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo { +internal fun VerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo { val extras = Bundle().apply { if (id_token.isNotEmpty()) { putString("id_token", id_token) @@ -110,7 +79,7 @@ private fun VerifiedPhoneNumber.toPhoneNumberInfo(): PhoneNumberInfo { ) } -private fun VerifiedPhoneNumber.toPhoneNumberVerification(): PhoneNumberVerification { +internal fun VerifiedPhoneNumber.toPhoneNumberVerification(): PhoneNumberVerification { val extras = Bundle().apply { putInt("rcs_state", rcs_state.value) } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt index c4a53de4f3..e3684cf4ae 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt @@ -1,6 +1,5 @@ package org.microg.gms.constellation.core -import com.squareup.wire.Duration import com.squareup.wire.GrpcClient import okhttp3.OkHttpClient import org.microg.gms.common.Constants diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 848978f6c9..6456cbb41b 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -173,11 +173,15 @@ private suspend fun handleVerifyPhoneNumberRequest( localReadFallback: Boolean = false, legacyCallbackOnFullFlow: Boolean = false ) { - try { + var phoneNumbers = emptyList() + var verifications = emptyArray() + val status = try { when (readCallbackMode) { ReadCallbackMode.LEGACY -> { Log.d(TAG, "Using read-only mode") - handleGetVerifiedPhoneNumbers(context, callbacks, request.extras) + phoneNumbers = fetchVerifiedPhoneNumbers(context, request.extras) + .map { it.toPhoneNumberInfo() } + Status.SUCCESS } ReadCallbackMode.TYPED -> { @@ -186,68 +190,46 @@ private suspend fun handleVerifyPhoneNumberRequest( } else { Log.d(TAG, "Using typed read-only mode") } - val response = fetchVerifiedPhoneNumbers(context, request.extras, callingPackage) - .toVerifyPhoneNumberResponse() - callbacks.onPhoneNumberVerificationsCompleted( - Status.SUCCESS, - response, - ApiMetadata.DEFAULT - ) + verifications = fetchVerifiedPhoneNumbers(context, request.extras, callingPackage) + .map { it.toPhoneNumberVerification() } + .toTypedArray() + Status.SUCCESS } ReadCallbackMode.NONE -> { Log.d(TAG, "Using full verification mode") - runVerificationFlow( - context, - request, - callingPackage, - callbacks, - legacyCallback = legacyCallbackOnFullFlow - ) + verifications = runVerificationFlow(context, request, callingPackage) + Status.SUCCESS } } } catch (e: Exception) { Log.e(TAG, "verifyPhoneNumber failed", e) - val status = when { + when { readCallbackMode != ReadCallbackMode.NONE -> Status.INTERNAL_ERROR e is GrpcException -> handleRpcError(e) e is NoConsentException -> Status(5001) else -> Status.INTERNAL_ERROR } - when (readCallbackMode) { - ReadCallbackMode.LEGACY -> { - callbacks.onPhoneNumberVerified( - status, - emptyList(), - ApiMetadata.DEFAULT - ) - } - - ReadCallbackMode.NONE -> { - if (legacyCallbackOnFullFlow) { - callbacks.onPhoneNumberVerified( - status, - emptyList(), - ApiMetadata.DEFAULT - ) - } else { - callbacks.onPhoneNumberVerificationsCompleted( - status, - VerifyPhoneNumberResponse(emptyArray(), Bundle()), - ApiMetadata.DEFAULT - ) - } - } + } - ReadCallbackMode.TYPED -> { - callbacks.onPhoneNumberVerificationsCompleted( - status, - VerifyPhoneNumberResponse(emptyArray(), Bundle()), - ApiMetadata.DEFAULT - ) - } + 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 { @@ -268,10 +250,8 @@ private class NoConsentException : Exception("No consent") private suspend fun runVerificationFlow( context: Context, request: VerifyPhoneNumberRequest, - callingPackage: String, - callbacks: IConstellationCallbacks, - legacyCallback: Boolean -) { + callingPackage: String +): Array { val sessionId = UUID.randomUUID().toString() val imsiToInfoMap = buildImsiToSubscriptionInfoMap(context) @@ -281,7 +261,6 @@ private suspend fun runVerificationFlow( "RCS" -> AsterismClient.RCS else -> AsterismClient.UNKNOWN_CLIENT } - if ( request.extras.getString("one_time_verification") != "True" && asterismClient != AsterismClient.UNKNOWN_CLIENT @@ -331,19 +310,7 @@ private suspend fun runVerificationFlow( MtSmsInboxRegistry.dispose() } - if (legacyCallback) { - callbacks.onPhoneNumberVerified( - Status.SUCCESS, - verifications.mapNotNull { it.toLegacyPhoneNumberInfoOrNull() }, - ApiMetadata.DEFAULT - ) - } else { - callbacks.onPhoneNumberVerificationsCompleted( - Status.SUCCESS, - VerifyPhoneNumberResponse(verifications, Bundle()), - ApiMetadata.DEFAULT - ) - } + return verifications } private fun PhoneNumberVerification.toLegacyPhoneNumberInfoOrNull(): PhoneNumberInfo? { @@ -464,4 +431,4 @@ suspend fun getConsent( } throw e } -} \ No newline at end of file +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt index e121a44dfe..9fa526aab7 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/ChallengeProcessor.kt @@ -87,51 +87,49 @@ object ChallengeProcessor { if (challenge.type != VerificationMethod.MO_SMS) { moSession = null } - if (challenge.type != VerificationMethod.CARRIER_ID || challenge.ts43_challenge != 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 { - val activeSession = - carrierIdSession?.takeIf { it.matches(challengeId, subId) } - ?: CarrierIdSession(challengeId, subId).also { - carrierIdSession = it - } - - activeSession.attempts += 1 + VerificationMethod.CARRIER_ID -> if (challenge.ts43_challenge != null) + challenge.ts43_challenge.verify(context, subId) + else { + carrierIdSession!!.attempts += 1 - if (activeSession.attempts >= 4) { - Log.w( - TAG, - "Attempt $attempt: Carrier ID challenge $challengeId is still pending after retry-exceeded response. Stopping." - ) - return currentVerification - } + if (carrierIdSession!!.attempts >= 4) { + Log.w( + TAG, + "Attempt $attempt: Carrier ID challenge $challengeId is still pending after retry-exceeded response. Stopping." + ) + return currentVerification + } - if (activeSession.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) - } + 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 -> - val activeSession = moSession?.takeIf { it.matches(challengeId, subId) } - if (activeSession != null) { - val delayMillis = activeSession.nextPollingDelayMillis(remainingMillis) + 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, @@ -139,11 +137,7 @@ object ChallengeProcessor { ) delay(delayMillis) } - activeSession.response - } else { - moChallenge.startSession(context, challengeId, subId).also { - moSession = it - }.response + moSession!!.response } } @@ -153,55 +147,12 @@ object ChallengeProcessor { ) else -> { - moSession = null Log.w(TAG, "Unsupported verification method: ${challenge.type}") null } } - if (challengeResponse != null) { - 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) { - 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 - } - } - } else { + if (challengeResponse == null) { Log.w( TAG, "Attempt $attempt: Challenge verification failed or returned no response." @@ -209,9 +160,51 @@ object ChallengeProcessor { // 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 } -} \ No newline at end of file +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt index 458712c4cb..58ab641b17 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MoSmsVerifier.kt @@ -16,6 +16,7 @@ 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 @@ -76,18 +77,21 @@ private suspend fun MoChallenge.sendOnce(context: Context, subId: Int): Challeng 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 = if (subId != -1 && !isActiveSubscription(context, subId)) { - MOChallengeResponseData.Status.NO_ACTIVE_SUBSCRIPTION - } else { - MOChallengeResponseData.Status.NO_SMS_MANAGER - } + status = MOChallengeResponseData.Status.NO_SMS_MANAGER ) ) val port = data_sms_info?.destination_port ?: 0 - val isBinarySms = port > 0 val action = ACTION_MO_SMS_SENT val messageId = UUID.randomUUID().toString() val sentIntent = Intent(action).apply { @@ -104,43 +108,9 @@ private suspend fun MoChallenge.sendOnce(context: Context, subId: Int): Challeng Log.d(TAG, "Sending MO SMS to $proxy_number with messageId: $messageId") - return withTimeoutOrNull(30_000L) { + val response = withTimeoutOrNull(30_000L) { suspendCancellableCoroutine { continuation -> - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == action) { - val receivedId = intent.getStringExtra("message_id") - if (receivedId != messageId) return - - val resultCode = resultCode - val errorCode = intent.getIntExtra("errorCode", -1) - - Log.d(TAG, "MO SMS sent result: $resultCode, error: $errorCode") - - val status = when (resultCode) { - -1 -> MOChallengeResponseData.Status.COMPLETED - else -> MOChallengeResponseData.Status.FAILED_TO_SEND_MO - } - - try { - context.unregisterReceiver(this) - } catch (_: Exception) { - } - - if (continuation.isActive) { - continuation.resume( - ChallengeResponse( - mo_response = MOChallengeResponseData( - status = status, - sms_result_code = resultCode.toLong(), - sms_error_code = errorCode.toLong() - ) - ) - ) - } - } - } - } + val receiver = MoSmsSentReceiver(action, messageId, continuation) ContextCompat.registerReceiver( context, @@ -150,14 +120,11 @@ private suspend fun MoChallenge.sendOnce(context: Context, subId: Int): Challeng ) continuation.invokeOnCancellation { - try { - context.unregisterReceiver(receiver) - } catch (_: Exception) { - } + runCatching { context.unregisterReceiver(receiver) } } try { - if (isBinarySms) { + if (port > 0) { smsManager.sendDataMessage( proxy_number, null, @@ -177,16 +144,46 @@ private suspend fun MoChallenge.sendOnce(context: Context, subId: Int): Challeng } } catch (e: Exception) { Log.e(TAG, "Failed to initiate MO SMS send", e) - try { - context.unregisterReceiver(receiver) - } catch (_: Exception) { - } + runCatching { context.unregisterReceiver(receiver) } if (continuation.isActive) { continuation.resume(failedMoResponse()) } } } - } ?: failedMoResponse() + } + return response ?: failedMoResponse() +} + +private class MoSmsSentReceiver( + private val action: String, + private val messageId: String, + private val continuation: CancellableContinuation +) : 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 { @@ -238,10 +235,6 @@ private fun isActiveSubscription(context: Context, subId: Int): Boolean { @Suppress("DEPRECATION") private fun resolveSmsManager(context: Context, subId: Int): SmsManager? { - if (subId != -1 && !isActiveSubscription(context, subId)) { - return null - } - return try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val manager = context.getSystemService() diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt index b010277b41..f394f7c11c 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/RegisteredSmsVerifier.kt @@ -116,7 +116,6 @@ private fun getLocalNumbers(context: Context, targetSubId: Int): List { context, Manifest.permission.READ_PHONE_STATE ) == PackageManager.PERMISSION_GRANTED - val isCarrier = telephonyManager?.hasCarrierPrivileges() == true val hasNumbers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ContextCompat.checkSelfPermission( context, @@ -126,7 +125,7 @@ private fun getLocalNumbers(context: Context, targetSubId: Int): List { hasState } - if (!isCarrier && (!hasState || !hasNumbers)) { + if (telephonyManager?.hasCarrierPrivileges() != true && (!hasState || !hasNumbers)) { Log.e(TAG, "Permission not granted") return emptyList() } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt index a43ef69f95..e9b309d7d1 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/Ts43Verifier.kt @@ -40,340 +40,333 @@ import javax.xml.parsers.DocumentBuilderFactory private const val TAG = "Ts43Verifier" fun Ts43Challenge.verify(context: Context, subId: Int): ChallengeResponse { - val verifier = InternalTs43Verifier(context, subId) - return verifier.verify(this) -} - -private class InternalTs43Verifier(private val context: Context, private val subId: Int) { - private val httpHistory = mutableListOf() - - private val telephonyManager: TelephonyManager by lazy { - val tm = requireNotNull(context.getSystemService()) { - "TelephonyManager unavailable" - } - if (subId >= 0) { - tm.createForSubscriptionId(subId) - } else tm + val baseTelephonyManager = requireNotNull(context.getSystemService()) { + "TelephonyManager unavailable" } - - private val eapAkaService by lazy { EapAkaService(telephonyManager) } - - fun verify(challenge: Ts43Challenge): ChallengeResponse { - httpHistory.clear() - - return try { - when { - challenge.client_challenge != null -> { - val op = challenge.client_challenge.operation - Log.d(TAG, "TS43: Client ODSA challenge route: ${op?.operation}") - if (op == null) { - buildFailureResponse( - challenge, - Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET - ) - } else { - val requestPayload = buildOdsaRequestPayload( - challenge.entitlement_url, - challenge.eap_aka_realm, - challenge.service_entitlement_request, - challenge.app_id - ) - if (requestPayload == null) { - buildFailureResponse( - challenge, - Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR - ) - } else { - val responseBody = performOdsaRequest( - challenge.entitlement_url, - op, - requestPayload, - challenge.app_id, - Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_GET_PHONE_NUMBER_API - ) - ChallengeResponse( - ts43_challenge_response = Ts43ChallengeResponse( - ts43_type = challenge.ts43_type, - client_challenge_response = ClientChallengeResponse( - get_phone_number_response = responseBody - ), - http_history = httpHistory.toList() - ) - ) - } - } - } - - challenge.server_challenge != null -> { - val op = challenge.server_challenge.operation - Log.d(TAG, "TS43: Server ODSA challenge route: ${op?.operation}") - if (op == null) { - buildFailureResponse( - challenge, - Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET - ) - } else { - val requestPayload = buildOdsaRequestPayload( - challenge.entitlement_url, - challenge.eap_aka_realm, - challenge.service_entitlement_request, - challenge.app_id - ) - if (requestPayload == null) { - buildFailureResponse( - challenge, - Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR - ) - } else { - val responseBody = performOdsaRequest( - challenge.entitlement_url, - op, - requestPayload, - challenge.app_id, - Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_ACQUIRE_TEMPORARY_TOKEN_API - ) - ChallengeResponse( - ts43_challenge_response = Ts43ChallengeResponse( - ts43_type = challenge.ts43_type, - server_challenge_response = ServerChallengeResponse( - acquire_temporary_token_response = responseBody - ), - http_history = httpHistory.toList() - ) - ) - } - } - } - - else -> buildFailureResponse( - challenge, - Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET + val telephonyManager = if (subId >= 0) { + baseTelephonyManager.createForSubscriptionId(subId) + } else { + baseTelephonyManager + } + val eapAkaService = EapAkaService(telephonyManager) + val okHttpClient = newTs43HttpClient() + val httpHistory = mutableListOf() + val op: OdsaOperation? + val requestType: Ts43ChallengeResponseError.RequestType + val routeName: String + val responseBuilder: (String, List) -> 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 ) } - } catch (e: Ts43ApiException) { - Log.w(TAG, "TS43 API failure requestType=${e.requestType} http=${e.httpStatus}", e) - ChallengeResponse( - ts43_challenge_response = Ts43ChallengeResponse( - ts43_type = challenge.ts43_type, - status = Ts43ChallengeResponseStatus( - error = Ts43ChallengeResponseError( - error_code = e.errorCode, - http_status = e.httpStatus, - request_type = e.requestType - ) + } + + 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 = httpHistory.toList() + http_history = history ) - ) - } catch (e: NullPointerException) { - Log.e(TAG, "TS43 null failure", e) - buildFailureResponse( - challenge, - Ts43ChallengeResponseStatus.Code.TS43_STATUS_INTERNAL_ERROR - ) - } catch (e: RuntimeException) { - Log.e(TAG, "TS43 runtime failure", e) - buildFailureResponse( - challenge, - Ts43ChallengeResponseStatus.Code.TS43_STATUS_RUNTIME_ERROR + } + } + + else -> { + return failureResponse( + this, + Ts43ChallengeResponseStatus.Code.TS43_STATUS_CHALLENGE_NOT_SET, + httpHistory ) } } - private fun performOdsaRequest( - entitlementUrl: String, - op: OdsaOperation, - req: ServiceEntitlementRequest?, - challengeAppId: String?, - requestType: Ts43ChallengeResponseError.RequestType - ): String { - val builder = op.builder(telephonyManager, req, currentAppIds(challengeAppId)) - 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 { + 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 + ) - 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 + 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 ) - } else { - Log.w(TAG, "ODSA request failed: ${response.code} for ${op.operation}") - throw Ts43ApiException( - errorCode = errorCode(31), + ), + 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, + 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, + 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 ODSA request", e) + Log.e(TAG, "Network error in EAP round $round", e) throw Ts43ApiException( errorCode = errorCode(30), httpStatus = -1, - requestType = requestType, + requestType = Ts43ChallengeResponseError.RequestType.TS43_REQUEST_TYPE_AUTH_API, cause = e ) } - } - - private fun buildOdsaRequestPayload( - 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 builder = req.builder(telephonyManager, eapId, currentAppIds(challengeAppId)) - - val initialUrl = builder.buildBaseUrl(entitlementUrl) - val postUrl = entitlementUrl - val userAgent = req.userAgent(context) - val acceptLanguage = Locale.getDefault().toLanguageTag().ifEmpty { "en-US" } - - // 1. Initial Identity Probe (GET) - 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" - - // 2. EAP Rounds Loop - 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 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 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 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 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() - ) + 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 + ) + .build() + httpHistory += "POST $postUrl" } - private fun currentAppIds(challengeAppId: String? = null): List { - return listOfNotNull( - challengeAppId?.takeIf { it.isNotBlank() } - ).ifEmpty { - listOf("ap2014") - } - } + return null +} - private fun extractEapRelayPacket(body: String): String? { - return runCatching { JSONObject(body).optString("eap-relay-packet") } - .getOrNull() - ?.takeIf { it.isNotEmpty() } - } +private fun extractEapRelayPacket(body: String): String? { + return runCatching { JSONObject(body).optString("eap-relay-packet") } + .getOrNull() + ?.takeIf { it.isNotEmpty() } +} - private fun extractAuthToken(body: String): String? { - runCatching { - JSONObject(body).optJSONObject("Token")?.optString("token") - }.getOrNull()?.takeIf { it.isNotEmpty() }?.let { return it } - - return runCatching { - val normalized = body - .replace("&", "&") - .replace("&amp;", "&") - .replace("\r\n", "\n") - val doc = DocumentBuilderFactory.newInstance() - .newDocumentBuilder() - .parse(InputSource(StringReader(normalized))) - val parms = doc.getElementsByTagName("parm") - for (i in 0 until parms.length) { - val node = parms.item(i) - val attrs = node.attributes ?: continue - val name = attrs.getNamedItem("name")?.nodeValue ?: continue - if (name == "token") { - return@runCatching attrs.getNamedItem("value")?.nodeValue - } +private fun extractAuthToken(body: String): String? { + runCatching { + JSONObject(body).optJSONObject("Token")?.optString("token") + }.getOrNull()?.takeIf { it.isNotEmpty() }?.let { return it } + + return runCatching { + val normalized = body + .replace("&", "&") + .replace("&amp;", "&") + .replace("\r\n", "\n") + val doc = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(InputSource(StringReader(normalized))) + val parms = doc.getElementsByTagName("parm") + for (i in 0 until parms.length) { + val node = parms.item(i) + val attrs = node.attributes ?: continue + val name = attrs.getNamedItem("name")?.nodeValue ?: continue + if (name == "token") { + return@runCatching attrs.getNamedItem("value")?.nodeValue } - null - }.getOrNull()?.takeIf { it.isNotEmpty() } - } + } + null + }.getOrNull()?.takeIf { it.isNotEmpty() } +} - private fun buildFailureResponse( - challenge: Ts43Challenge, - statusCode: Ts43ChallengeResponseStatus.Code - ): ChallengeResponse { - return ChallengeResponse( - ts43_challenge_response = Ts43ChallengeResponse( - ts43_type = challenge.ts43_type, - status = Ts43ChallengeResponseStatus(status_code = statusCode), - http_history = httpHistory.toList() - ) +private fun failureResponse( + challenge: Ts43Challenge, + statusCode: Ts43ChallengeResponseStatus.Code, + httpHistory: List +): ChallengeResponse { + return ChallengeResponse( + ts43_challenge_response = Ts43ChallengeResponse( + ts43_type = challenge.ts43_type, + status = Ts43ChallengeResponseStatus(status_code = statusCode), + http_history = httpHistory.toList() ) - } + ) } private class Ts43ApiException( @@ -387,14 +380,15 @@ private fun errorCode(rawCode: Int): Ts43ChallengeResponseError.Code = Ts43ChallengeResponseError.Code.fromValue(rawCode) ?: Ts43ChallengeResponseError.Code.TS43_ERROR_CODE_UNSPECIFIED -private val okHttpClient by lazy { - OkHttpClient.Builder() +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>() + override fun saveFromResponse(url: HttpUrl, cookies: List) { val existing = cookieStore.getOrPut(url.host) { mutableListOf() } for (cookie in cookies) { From 44ffa11c82c4ecb9a4220066e067b0106a4c1872 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 15 May 2026 20:39:15 +0800 Subject: [PATCH 36/63] Replicate GMS behavior better --- play-services-constellation/core/src/main/AndroidManifest.xml | 3 +++ .../gms/constellation/core/proto/builder/SyncRequestBuilder.kt | 3 ++- .../gms/constellation/core/verification/MtSmsVerifier.kt | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml index 943034c187..4b9166754e 100644 --- a/play-services-constellation/core/src/main/AndroidManifest.xml +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -18,6 +18,9 @@ + diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt index b68f0d5991..57612ad92a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt @@ -8,6 +8,7 @@ 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 @@ -160,7 +161,7 @@ suspend operator fun SyncRequest.Companion.invoke( val verifications = imsiToInfoMap.map { (imsi, subscriptionInfo) -> val subscriptionId = subscriptionInfo.subscriptionId val slotIndex = subscriptionInfo.simSlotIndex - val phoneNumber = subscriptionInfo.number ?: "" + val phoneNumber = PhoneNumberUtils.formatNumberToE164(subscriptionInfo.number, subscriptionInfo.countryIso) val iccid = subscriptionInfo.iccId ?: "" Verification( diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index 7a522c1e18..060574ecbe 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -112,7 +112,7 @@ internal class MtSmsInbox( context, receiver, filter, - ContextCompat.RECEIVER_NOT_EXPORTED + ContextCompat.RECEIVER_EXPORTED ) } From ac8f7a67e752b9168a6b0bfe2d9ea76e6372f4b7 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 16 May 2026 14:01:03 +0800 Subject: [PATCH 37/63] Missed null check --- .../gms/constellation/core/proto/builder/SyncRequestBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt index 57612ad92a..1c675a0875 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt @@ -161,7 +161,7 @@ suspend operator fun SyncRequest.Companion.invoke( val verifications = imsiToInfoMap.map { (imsi, subscriptionInfo) -> val subscriptionId = subscriptionInfo.subscriptionId val slotIndex = subscriptionInfo.simSlotIndex - val phoneNumber = PhoneNumberUtils.formatNumberToE164(subscriptionInfo.number, subscriptionInfo.countryIso) + val phoneNumber = PhoneNumberUtils.formatNumberToE164(subscriptionInfo.number, subscriptionInfo.countryIso) ?: "" val iccid = subscriptionInfo.iccId ?: "" Verification( From 23bf3071971709e7c8d7f579bfca09f2888abbe0 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 16 May 2026 17:58:35 +0800 Subject: [PATCH 38/63] Set priority of SMS receiver to 1000 --- .../gms/constellation/core/verification/MtSmsVerifier.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index 060574ecbe..d4beb0179a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -107,7 +107,9 @@ internal class MtSmsInbox( } init { - val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) + val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION).apply { + priority = 1000 + } ContextCompat.registerReceiver( context, receiver, From 327b5b0cc1870e4d397460340febb70e91d8a0c0 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 17 May 2026 03:53:40 +0800 Subject: [PATCH 39/63] Add Phenotype flag to prevent ACS fallback --- .../main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt index c925faf26f..f4599d4ab3 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt @@ -78,6 +78,9 @@ private val CONFIGURATION_OPTIONS = mapOf( "com.google.android.apps.photos" to arrayOf( Flag("45617431", true, 0), ), + "com.google.android.ims.library" to arrayOf( + Flag("RcsProvisioning__min_gmscore_version_for_upi_without_acs_fallback_met", true, 0) + ) ) class PhenotypeServiceImpl(val packageName: String?) : IPhenotypeService.Stub() { From 309566d04fb0b9de8965b038b2c8ebc1c4cb8631 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 17 May 2026 04:15:53 +0800 Subject: [PATCH 40/63] Set timeout on the right place --- .../main/kotlin/org/microg/gms/constellation/core/RpcClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt index e3684cf4ae..5513630793 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/RpcClient.kt @@ -9,7 +9,7 @@ import java.util.concurrent.TimeUnit object RpcClient { private val client: OkHttpClient = OkHttpClient.Builder() - .callTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) .addInterceptor { chain -> val originalRequest = chain.request() val builder = originalRequest.newBuilder() From 4510ced43d7c8be86785d98314ec000b4ca22e81 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Mon, 18 May 2026 04:40:11 +0800 Subject: [PATCH 41/63] Add MLS Phenotype flags --- .../main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt index f4599d4ab3..53e73a0a0e 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt @@ -80,6 +80,10 @@ private val CONFIGURATION_OPTIONS = mapOf( ), "com.google.android.ims.library" to arrayOf( Flag("RcsProvisioning__min_gmscore_version_for_upi_without_acs_fallback_met", true, 0) + ), + "com.google.android.apps.messaging#com.google.android.apps.messaging" to arrayOf( + Flag("bugle_phenotype__enableDownloadCertificatesForMlsProvisioning", true, 0), + Flag("bugle_phenotype__enableUploadKeyPackagesForMlsProvisioning", true, 0), ) ) From ae6ae71076f7ace40cb3073b30652fff1774515a Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:48:57 -0400 Subject: [PATCH 42/63] Implement `play-services-asterism` --- play-services-asterism/build.gradle | 52 +++++ .../src/main/AndroidManifest.xml | 17 ++ .../asterism/GetAsterismConsentRequest.aidl | 3 + .../asterism/GetAsterismConsentResponse.aidl | 3 + .../asterism/SetAsterismConsentRequest.aidl | 3 + .../asterism/SetAsterismConsentResponse.aidl | 3 + .../internal/IAsterismApiService.aidl | 19 ++ .../asterism/internal/IAsterismCallbacks.aidl | 12 + .../gms/asterism/GetAsterismConsent.kt | 57 +++++ .../gms/asterism/SetAsterismConsent.kt | 106 +++++++++ .../microg/gms/asterism/AsterismApiService.kt | 84 +++++++ .../gms/asterism/GetAsterismConsentHandler.kt | 89 ++++++++ .../gms/asterism/SetAsterismConsentHandler.kt | 206 ++++++++++++++++++ .../proto/builders/OnDemandConsentBuilder.kt | 72 ++++++ play-services-core/build.gradle | 1 + .../src/main/AndroidManifest.xml | 1 - .../gms/auth/AuthManagerServiceImpl.java | 3 + settings.gradle | 1 + 18 files changed, 731 insertions(+), 1 deletion(-) create mode 100644 play-services-asterism/build.gradle create mode 100644 play-services-asterism/src/main/AndroidManifest.xml create mode 100644 play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentRequest.aidl create mode 100644 play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentResponse.aidl create mode 100644 play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentRequest.aidl create mode 100644 play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentResponse.aidl create mode 100644 play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismApiService.aidl create mode 100644 play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismCallbacks.aidl create mode 100644 play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt create mode 100644 play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt create mode 100644 play-services-asterism/src/main/kotlin/org/microg/gms/asterism/AsterismApiService.kt create mode 100644 play-services-asterism/src/main/kotlin/org/microg/gms/asterism/GetAsterismConsentHandler.kt create mode 100644 play-services-asterism/src/main/kotlin/org/microg/gms/asterism/SetAsterismConsentHandler.kt create mode 100644 play-services-asterism/src/main/kotlin/org/microg/gms/asterism/proto/builders/OnDemandConsentBuilder.kt diff --git a/play-services-asterism/build.gradle b/play-services-asterism/build.gradle new file mode 100644 index 0000000000..2d93846553 --- /dev/null +++ b/play-services-asterism/build.gradle @@ -0,0 +1,52 @@ +/* + * 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: 'kotlin-parcelize' +apply plugin: 'kotlin-kapt' +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 + } + + kotlinOptions { + jvmTarget = 1.8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG service implementation for play-services-asterism' + +dependencies { + implementation project(':play-services-base-core') + implementation project(':play-services-constellation') + implementation project(':play-services-iid') + + implementation project(':play-services-droidguard') + implementation project(':play-services-tasks-ktx') + + kapt project(":safe-parcel-processor") +} diff --git a/play-services-asterism/src/main/AndroidManifest.xml b/play-services-asterism/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bf92ddcaf3 --- /dev/null +++ b/play-services-asterism/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentRequest.aidl b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentRequest.aidl new file mode 100644 index 0000000000..d7ee6f0a1a --- /dev/null +++ b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.asterism; + +parcelable GetAsterismConsentRequest; diff --git a/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentResponse.aidl b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentResponse.aidl new file mode 100644 index 0000000000..3494b9bfc2 --- /dev/null +++ b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/GetAsterismConsentResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.asterism; + +parcelable GetAsterismConsentResponse; diff --git a/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentRequest.aidl b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentRequest.aidl new file mode 100644 index 0000000000..7f8edb6a1d --- /dev/null +++ b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.asterism; + +parcelable SetAsterismConsentRequest; diff --git a/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentResponse.aidl b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentResponse.aidl new file mode 100644 index 0000000000..be9707048e --- /dev/null +++ b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/SetAsterismConsentResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.asterism; + +parcelable SetAsterismConsentResponse; diff --git a/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismApiService.aidl b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismApiService.aidl new file mode 100644 index 0000000000..75eddea284 --- /dev/null +++ b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismApiService.aidl @@ -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 + ); +} diff --git a/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismCallbacks.aidl b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismCallbacks.aidl new file mode 100644 index 0000000000..40d41c8375 --- /dev/null +++ b/play-services-asterism/src/main/aidl/com/google/android/gms/asterism/internal/IAsterismCallbacks.aidl @@ -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); +} \ No newline at end of file diff --git a/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt b/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt new file mode 100644 index 0000000000..d2f3139c4d --- /dev/null +++ b/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt @@ -0,0 +1,57 @@ +package com.google.android.gms.asterism + +import android.os.Parcel +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.SafeParcelable.Constructor +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param +import org.microg.gms.constellation.proto.AsterismClient +import org.microg.gms.constellation.proto.Consent +import org.microg.gms.constellation.proto.ConsentVersion + +@SafeParcelable.Class +data class GetAsterismConsentRequest @Constructor constructor( + @JvmField @Param(1) @Field(1) val requestCode: Int, + @JvmField @Param(2) @Field(2) val asterismClientValue: Int +) : AbstractSafeParcelable() { + val asterismClient: AsterismClient + get() = AsterismClient.fromValue(asterismClientValue) ?: AsterismClient.UNKNOWN_CLIENT + + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(GetAsterismConsentRequest::class.java) + } +} + +@SafeParcelable.Class +data class GetAsterismConsentResponse @Constructor constructor( + @JvmField @Param(1) @Field(1) val requestCode: Int, + @JvmField @Param(2) @Field(2) val consentStateValue: Int, + @JvmField @Param(3) @Field(3) val gmscoreIidToken: String?, + @JvmField @Param(4) @Field(4) val fid: String?, + @JvmField @Param(5) @Field(5) val consentTypeValue: Int +) : AbstractSafeParcelable() { + constructor( + requestCode: Int, + consentState: Consent, + gmscoreIidToken: String?, + fid: String?, + consentVersion: ConsentVersion + ) : this( + requestCode, + if (consentState == Consent.CONSENTED || consentState == Consent.CONSENT_UNKNOWN) consentState.value else Consent.NO_CONSENT.value, + gmscoreIidToken, + fid, + consentVersion.value + ) + + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(GetAsterismConsentResponse::class.java) + } +} diff --git a/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt b/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt new file mode 100644 index 0000000000..93f8619690 --- /dev/null +++ b/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt @@ -0,0 +1,106 @@ +package com.google.android.gms.asterism + +import android.os.Bundle +import android.os.Parcel +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.SafeParcelable.Constructor +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field +import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param +import org.microg.gms.constellation.proto.AsterismClient +import org.microg.gms.constellation.proto.Consent +import org.microg.gms.constellation.proto.ConsentSource +import org.microg.gms.constellation.proto.ConsentVersion +import org.microg.gms.constellation.proto.FlowContext + +@SafeParcelable.Class +data class SetAsterismConsentRequest +@Constructor +constructor( + @JvmField @Param(1) @Field(1) val requestCode: Int, + @JvmField @Param(2) @Field(2) val asterismClientValue: Int, + @JvmField @Param(3) @Field(3) val flowContextValue: Int, + @JvmField @Param(4) @Field(value = 4, type = "int[]") val tosResourceIds: IntArray?, + @JvmField @Param(5) @Field(5) val timestamp: Long?, + @JvmField @Param(6) @Field(6) val consentValue: Int, + @JvmField @Param(7) @Field(7) val extras: Bundle?, + @JvmField @Param(8) @Field(8) val statusValue: Int, + @JvmField @Param(9) @Field(9) val tosUrl: String?, + @JvmField @Param(10) @Field(10) val language: String?, + @JvmField @Param(11) @Field(11) val country: String?, + @JvmField @Param(12) @Field(12) val tosVersion: String?, + @JvmField @Param(13) @Field(13) val tosContentTitle: String?, + @JvmField @Param(14) @Field(14) val accountName: String?, + @JvmField @Param(15) @Field(15) val consentVariant: String?, + @JvmField @Param(16) @Field(16) val consentTrigger: String?, + @JvmField @Param(17) @Field(17) val rcsFlowContextValue: Int, + @JvmField @Param(18) @Field(18) val deviceConsentSourceValue: Int, + @JvmField @Param(19) @Field(19) val deviceConsentVersionValue: Int, +) : AbstractSafeParcelable() { + enum class Status(val value: Int) { + RCS_DEFAULT(0), + RCS_LEGAL_FYI(1), + DEVICE_PNVR(2), + ON_DEMAND(3), + EXPIRED(4); + + companion object { + fun fromValue(value: Int): Status = entries.find { it.value == value } ?: EXPIRED + } + } + + val asterismClient: AsterismClient + get() = AsterismClient.fromValue(asterismClientValue) ?: AsterismClient.UNKNOWN_CLIENT + + val consent: Consent + get() = Consent.fromValue(consentValue) ?: Consent.CONSENT_UNKNOWN + + val rcsFlowContext: FlowContext + get() = FlowContext.fromValue(rcsFlowContextValue) ?: FlowContext.FLOW_CONTEXT_UNSPECIFIED + + val deviceConsentSource: ConsentSource + get() = ConsentSource.fromValue(deviceConsentSourceValue) + ?: ConsentSource.SOURCE_UNSPECIFIED + + val deviceConsentVersion: ConsentVersion + get() = ConsentVersion.fromValue(deviceConsentVersionValue).let { + if (it == null || it == ConsentVersion.CONSENT_VERSION_UNSPECIFIED) { + ConsentVersion.PHONE_VERIFICATION_DEFAULT + } else { + it + } + } + + fun isDevicePnvrFlow(): Boolean { + return asterismClient == AsterismClient.CONSTELLATION && + deviceConsentSourceValue > 0 && + deviceConsentVersionValue > 0 + } + + val status: Status + get() = Status.fromValue(statusValue) + + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(SetAsterismConsentRequest::class.java) + } +} + +@SafeParcelable.Class +data class SetAsterismConsentResponse +@Constructor +constructor( + @JvmField @Param(1) @Field(1) val requestCode: Int, + @JvmField @Param(2) @Field(2) val gmscoreIidToken: String?, + @JvmField @Param(3) @Field(3) val fid: String? +) : AbstractSafeParcelable() { + + override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) + + companion object { + @JvmField + val CREATOR = findCreator(SetAsterismConsentResponse::class.java) + } +} diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/AsterismApiService.kt b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/AsterismApiService.kt new file mode 100644 index 0000000000..ea0c0c622d --- /dev/null +++ b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/AsterismApiService.kt @@ -0,0 +1,84 @@ +package org.microg.gms.asterism + +import android.content.Context +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 + serviceScope.launch { handleGetAsterismConsent(context, cb, request) } + } + + override fun setAsterismConsent(cb: IAsterismCallbacks?, request: SetAsterismConsentRequest?) { + Log.i(TAG, "setAsterismConsent(): $request") + if (cb == null || request == null) return + serviceScope.launch { handleSetAsterismConsent(context, cb, request) } + } + + override fun getIsPnvrConstellationDevice(cb: IAsterismCallbacks?) { + Log.i(TAG, "getIsPnvrConstellationDevice()") + if (cb == null) return + serviceScope.launch { handleGetIsPnvrConstellationDevice(context, cb) } + } +} diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/GetAsterismConsentHandler.kt b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/GetAsterismConsentHandler.kt new file mode 100644 index 0000000000..327c976617 --- /dev/null +++ b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/GetAsterismConsentHandler.kt @@ -0,0 +1,89 @@ +package org.microg.gms.asterism + +import android.content.Context +import android.util.Log +import com.google.android.gms.asterism.GetAsterismConsentRequest +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.AuthManager +import org.microg.gms.constellation.ConstellationStateStore +import org.microg.gms.constellation.RpcClient +import org.microg.gms.constellation.proto.Consent +import org.microg.gms.constellation.proto.ConsentVersion +import org.microg.gms.constellation.proto.DeviceID +import org.microg.gms.constellation.proto.GetConsentRequest +import org.microg.gms.constellation.proto.RequestHeader +import org.microg.gms.constellation.proto.builders.buildRequestContext +import org.microg.gms.constellation.proto.builders.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 = AuthManager.get(context) + 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), + 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) + } +} diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/SetAsterismConsentHandler.kt b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/SetAsterismConsentHandler.kt new file mode 100644 index 0000000000..e4c773b9c9 --- /dev/null +++ b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/SetAsterismConsentHandler.kt @@ -0,0 +1,206 @@ +package org.microg.gms.asterism + +import android.content.Context +import android.util.Log +import com.google.android.gms.asterism.SetAsterismConsentRequest +import com.google.android.gms.asterism.SetAsterismConsentResponse +import com.google.android.gms.asterism.internal.IAsterismCallbacks +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.AuthManager +import org.microg.gms.constellation.proto.AsterismClient +import org.microg.gms.constellation.proto.AsterismConsent +import org.microg.gms.constellation.proto.AuditToken +import org.microg.gms.constellation.proto.Consent +import org.microg.gms.constellation.proto.ConsentVersion +import org.microg.gms.constellation.proto.FlowContext +import org.microg.gms.constellation.proto.OnDemandConsent +import org.microg.gms.constellation.proto.Param +import org.microg.gms.constellation.proto.RcsConsent +import org.microg.gms.constellation.proto.RequestHeader +import org.microg.gms.constellation.proto.RequestTrigger +import org.microg.gms.constellation.proto.SetConsentRequest +import org.microg.gms.constellation.proto.builders.* +import org.microg.gms.asterism.rpc.builders.invoke +import org.microg.gms.constellation.ConstellationStateStore +import org.microg.gms.constellation.RpcClient +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 + } + + val isRcsSpecificContext = request.rcsFlowContext in listOf( + FlowContext.FLOW_CONTEXT_RCS_CONSENT, + FlowContext.FLOW_CONTEXT_RCS_DEFAULT_ON_OUT_OF_BOX, + FlowContext.FLOW_CONTEXT_RCS_SAMSUNG_UNFREEZE + ) + + if (request.asterismClient != AsterismClient.RCS && isRcsSpecificContext) { + Log.e(TAG, "RCS-only flow context cannot be used with non-RCS client") + callbacks.onConsentRegistered( + Status.INTERNAL_ERROR, + SetAsterismConsentResponse(request.requestCode, "", "") + ) + return@withContext + } + + if (request.extras != null) { + Log.d(TAG, "Extras keys: ${request.extras.keySet().joinToString()}") + for (key in request.extras.keySet()) { + val value = request.extras.get(key) + Log.d(TAG, " $key = $value") + } + } + + if (request.status == SetAsterismConsentRequest.Status.ON_DEMAND) { + if (request.accountName.isNullOrBlank() && request.extras?.getString("gaia_access_token") + .isNullOrBlank() + ) { + Log.e(TAG, "ODC missing accountName and no gaia_access_token in extras") + callbacks.onConsentRegistered( + Status.INTERNAL_ERROR, + SetAsterismConsentResponse(request.requestCode, "", "") + ) + return@withContext + } + } + + val extras = request.extras + val apiParams = Param.getList(extras) + + var deviceConsent: AsterismConsent? = null + var rcsConsent: RcsConsent? = null + var onDemandConsent: OnDemandConsent? = null + var flowContext: FlowContext = FlowContext.FLOW_CONTEXT_UNSPECIFIED + + when { + request.isDevicePnvrFlow() -> { + deviceConsent = AsterismConsent( + consent = request.consent, + consent_version = request.deviceConsentVersion, + consent_source = request.deviceConsentSource + ) + } + request.status == SetAsterismConsentRequest.Status.ON_DEMAND -> { + onDemandConsent = OnDemandConsent(context, 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.asterismClient == AsterismClient.CONSTELLATION) { + if (request.language.isNullOrBlank() || request.consentVariant.isNullOrBlank()) { + Log.w( + TAG, + "CONSTELLATION client requires language and consentVariant" + ) + Log.w( + TAG, + " language=${request.language}, consentVariant=${request.consentVariant}" + ) + callbacks.onConsentRegistered( + Status.INTERNAL_ERROR, + SetAsterismConsentResponse(request.requestCode, "", "") + ) + return@withContext + } + } + + val rcsConsentVersion = if (request.consent == Consent.CONSENTED) { + ConsentVersion.RCS_CONSENT + } else { + ConsentVersion.RCS_OUT_OF_BOX + } + + rcsConsent = RcsConsent( + consent = request.consent, + consent_version = rcsConsentVersion + ) + + flowContext = request.rcsFlowContext + } + } + + val needsAuditRecord = deviceConsent != null || onDemandConsent != null + val auditRecordBytes = if (needsAuditRecord) { + AuditToken.generate().encode().toByteString() + } else ByteString.EMPTY + + val authManager = AuthManager.get(context) + val buildContext = buildRequestContext(context, authManager) + val sessionId = UUID.randomUUID().toString() + + val protoRequest = SetConsentRequest( + header_ = RequestHeader( + context, + sessionId, + buildContext, + RequestTrigger.Type.CONSENT_API_TRIGGER + ), + asterism_client = request.asterismClient, + device_consent = deviceConsent, + rcs_consent = rcsConsent, + on_demand_consent = onDemandConsent, + flow_context = flowContext, + 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 == ConsentVersion.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, "", "") + ) + } +} diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/proto/builders/OnDemandConsentBuilder.kt b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/proto/builders/OnDemandConsentBuilder.kt new file mode 100644 index 0000000000..ae576343b8 --- /dev/null +++ b/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/proto/builders/OnDemandConsentBuilder.kt @@ -0,0 +1,72 @@ +package org.microg.gms.asterism.rpc.builders + +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 org.microg.gms.constellation.proto.Consent +import org.microg.gms.constellation.proto.GaiaToken +import org.microg.gms.constellation.proto.OnDemandConsent + +private const val ODC_TAG = "OnDemandConsent" + +@SuppressLint("MissingPermission") +operator fun OnDemandConsent.Companion.invoke( + context: Context, + request: SetAsterismConsentRequest, + extras: Bundle? +): OnDemandConsent? { + val isOnDemand = request.status == SetAsterismConsentRequest.Status.ON_DEMAND || + extras?.containsKey("consent_variant_key") == true || + extras?.containsKey("consent_trigger_key") == true || + extras?.containsKey("gaia_access_token") == true + + if (!isOnDemand) return null + + val consentVariant = extras?.getString("consent_variant_key") + ?: request.consentVariant + + val consentTrigger = extras?.getString("consent_trigger_key") + ?: request.consentTrigger + + var gaiaAccessToken = extras?.getString("gaia_access_token") + + if (gaiaAccessToken.isNullOrBlank()) { + val accountName = request.accountName + if (!accountName.isNullOrBlank()) { + 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(ODC_TAG, "Failed to get auth token for account $accountName: ${e.message}") + null + } + } + } + + if (gaiaAccessToken.isNullOrBlank()) { + Log.w(ODC_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 ?: "" + ) +} diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 362e39c76a..0b38b1058c 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -59,6 +59,7 @@ dependencies { implementation project(':play-services-appinvite') implementation project(':play-services-auth-base') implementation project(':play-services-auth') + implementation project(':play-services-asterism') implementation project(':play-services-clearcut') implementation project(':play-services-drive') implementation project(':play-services-games') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index a29fff819c..6c18b98a9a 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -1197,7 +1197,6 @@ - diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java index e0ae41cc9e..7563d49c43 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java @@ -240,7 +240,10 @@ public int hasCapabilities(HasCapabilitiesRequest request) throws RemoteExceptio PackageUtils.assertGooglePackagePermission(context, GooglePackagePermission.ACCOUNT); List services = Arrays.asList(AccountManager.get(context).getUserData(request.account, "services").split(",")); for (String capability : request.capabilities) { + // Required for com.google.android.apps.messaging to pass correct arguments to SetAsterismConsent + if (capability.equals("ge2tamrnmnqxa")) continue; if (capability.startsWith("service_") && !services.contains(capability.substring(8)) || !services.contains(capability)) { + Log.w(TAG, "Refusing, we do not have " + capability); return 6; } } diff --git a/settings.gradle b/settings.gradle index 9fcbcea289..ee0b621dda 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,6 +31,7 @@ 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' From ad3b5377505a597f7122ec433c019f790e8604d5 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:58:34 -0400 Subject: [PATCH 43/63] Refactoring --- play-services-asterism/build.gradle | 18 +-- play-services-asterism/core/build.gradle | 54 +++++++ .../core/src/main/AndroidManifest.xml | 15 ++ .../gms/asterism/GetAsterismConsent.kt | 31 ++++ .../gms/asterism/SetAsterismConsent.kt | 50 +++++++ .../gms/asterism/core}/AsterismApiService.kt | 6 +- .../core}/GetAsterismConsentHandler.kt | 35 +++-- .../core}/SetAsterismConsentHandler.kt | 82 ++++++----- .../proto/builder}/OnDemandConsentBuilder.kt | 19 +-- .../asterism/GetAsterismConsentRequest.java | 35 +++++ .../asterism/GetAsterismConsentResponse.java | 53 +++++++ .../asterism/SetAsterismConsentRequest.java | 133 ++++++++++++++++++ .../asterism/SetAsterismConsentResponse.java | 43 ++++++ .../gms/asterism/GetAsterismConsent.kt | 57 -------- .../gms/asterism/SetAsterismConsent.kt | 106 -------------- play-services-core/build.gradle | 1 + settings.gradle | 1 + 17 files changed, 503 insertions(+), 236 deletions(-) create mode 100644 play-services-asterism/core/build.gradle create mode 100644 play-services-asterism/core/src/main/AndroidManifest.xml create mode 100644 play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt create mode 100644 play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt rename play-services-asterism/{src/main/kotlin/org/microg/gms/asterism => core/src/main/kotlin/org/microg/gms/asterism/core}/AsterismApiService.kt (92%) rename play-services-asterism/{src/main/kotlin/org/microg/gms/asterism => core/src/main/kotlin/org/microg/gms/asterism/core}/GetAsterismConsentHandler.kt (72%) rename play-services-asterism/{src/main/kotlin/org/microg/gms/asterism => core/src/main/kotlin/org/microg/gms/asterism/core}/SetAsterismConsentHandler.kt (73%) rename play-services-asterism/{src/main/kotlin/org/microg/gms/asterism/proto/builders => core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder}/OnDemandConsentBuilder.kt (77%) create mode 100644 play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentRequest.java create mode 100644 play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentResponse.java create mode 100644 play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java create mode 100644 play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentResponse.java delete mode 100644 play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt delete mode 100644 play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt diff --git a/play-services-asterism/build.gradle b/play-services-asterism/build.gradle index 2d93846553..0f73a6901c 100644 --- a/play-services-asterism/build.gradle +++ b/play-services-asterism/build.gradle @@ -5,9 +5,6 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' apply plugin: 'signing' android { @@ -30,23 +27,14 @@ android { 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' +description = 'microG implementation of play-services-asterism' dependencies { - implementation project(':play-services-base-core') - implementation project(':play-services-constellation') - implementation project(':play-services-iid') - - implementation project(':play-services-droidguard') - implementation project(':play-services-tasks-ktx') + implementation project(':play-services-basement') - kapt project(":safe-parcel-processor") + annotationProcessor project(":safe-parcel-processor") } diff --git a/play-services-asterism/core/build.gradle b/play-services-asterism/core/build.gradle new file mode 100644 index 0000000000..eb06db5958 --- /dev/null +++ b/play-services-asterism/core/build.gradle @@ -0,0 +1,54 @@ +/* + * 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" + + buildFeatures { + aidl = true + } + + 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') + implementation project(':play-services-iid') + + implementation project(':play-services-droidguard') + implementation project(':play-services-tasks-ktx') +} diff --git a/play-services-asterism/core/src/main/AndroidManifest.xml b/play-services-asterism/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5031ca6820 --- /dev/null +++ b/play-services-asterism/core/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt b/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt new file mode 100644 index 0000000000..23a03bc9ab --- /dev/null +++ b/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt @@ -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 + ) +} \ No newline at end of file diff --git a/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt b/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt new file mode 100644 index 0000000000..c51bf22095 --- /dev/null +++ b/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt @@ -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.Consent +import org.microg.gms.constellation.core.proto.ConsentSource +import org.microg.gms.constellation.core.proto.ConsentVersion +import org.microg.gms.constellation.core.proto.FlowContext + +enum class SetAsterismConsentRequestStatus(val value: Int) { + RCS_DEFAULT(0), + RCS_LEGAL_FYI(1), + DEVICE_PNVR(2), + ON_DEMAND(3), + EXPIRED(4); + + companion object { + fun fromValue(value: Int): SetAsterismConsentRequestStatus = + entries.find { it.value == value } ?: EXPIRED + } +} + +val SetAsterismConsentRequest.asterismClient: AsterismClient + get() = AsterismClient.fromValue(asterismClientValue) ?: AsterismClient.UNKNOWN_CLIENT + +val SetAsterismConsentRequest.consent: Consent + get() = Consent.fromValue(consentValue) ?: Consent.CONSENT_UNKNOWN + +val SetAsterismConsentRequest.rcsFlowContext: FlowContext + get() = FlowContext.fromValue(rcsFlowContextValue) ?: FlowContext.FLOW_CONTEXT_UNSPECIFIED + +val SetAsterismConsentRequest.deviceConsentSource: ConsentSource + get() = ConsentSource.fromValue(deviceConsentSourceValue) ?: ConsentSource.SOURCE_UNSPECIFIED + +val SetAsterismConsentRequest.deviceConsentVersion: ConsentVersion + get() = ConsentVersion.fromValue(deviceConsentVersionValue).let { + if (it == null || it == ConsentVersion.CONSENT_VERSION_UNSPECIFIED) { + ConsentVersion.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 +} \ No newline at end of file diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/AsterismApiService.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/AsterismApiService.kt similarity index 92% rename from play-services-asterism/src/main/kotlin/org/microg/gms/asterism/AsterismApiService.kt rename to play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/AsterismApiService.kt index ea0c0c622d..4917d9070f 100644 --- a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/AsterismApiService.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/AsterismApiService.kt @@ -1,6 +1,7 @@ -package org.microg.gms.asterism +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 @@ -67,18 +68,21 @@ class AsterismApiServiceImpl( ) { 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) } } } diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/GetAsterismConsentHandler.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/GetAsterismConsentHandler.kt similarity index 72% rename from play-services-asterism/src/main/kotlin/org/microg/gms/asterism/GetAsterismConsentHandler.kt rename to play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/GetAsterismConsentHandler.kt index 327c976617..5c32952426 100644 --- a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/GetAsterismConsentHandler.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/GetAsterismConsentHandler.kt @@ -1,23 +1,28 @@ -package org.microg.gms.asterism +@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.GetAsterismConsentResponse +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.AuthManager -import org.microg.gms.constellation.ConstellationStateStore -import org.microg.gms.constellation.RpcClient -import org.microg.gms.constellation.proto.Consent -import org.microg.gms.constellation.proto.ConsentVersion -import org.microg.gms.constellation.proto.DeviceID -import org.microg.gms.constellation.proto.GetConsentRequest -import org.microg.gms.constellation.proto.RequestHeader -import org.microg.gms.constellation.proto.builders.buildRequestContext -import org.microg.gms.constellation.proto.builders.invoke +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.builder.buildRequestContext +import org.microg.gms.constellation.core.proto.builder.invoke import java.util.UUID private const val ASTERISM_TAG = "GetAsterismConsent" @@ -29,7 +34,7 @@ suspend fun handleGetAsterismConsent( request: GetAsterismConsentRequest ) = withContext(Dispatchers.IO) { try { - val authManager = AuthManager.get(context) + val authManager = context.authManager val buildContext = buildRequestContext(context, authManager) val response = RpcClient.phoneDeviceVerificationClient.GetConsent().execute( GetConsentRequest( @@ -50,7 +55,7 @@ suspend fun handleGetAsterismConsent( callbacks.onConsentFetched( Status.SUCCESS, - GetAsterismConsentResponse( + getAsterismConsentResponse( request.requestCode, consentValue, buildContext.iidToken, @@ -62,7 +67,7 @@ suspend fun handleGetAsterismConsent( Log.e(ASTERISM_TAG, "getAsterismConsent failed", e) callbacks.onConsentFetched( Status.INTERNAL_ERROR, - GetAsterismConsentResponse( + getAsterismConsentResponse( request.requestCode, Consent.CONSENT_UNKNOWN, null, diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/SetAsterismConsentHandler.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt similarity index 73% rename from play-services-asterism/src/main/kotlin/org/microg/gms/asterism/SetAsterismConsentHandler.kt rename to play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt index e4c773b9c9..b293ba2189 100644 --- a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/SetAsterismConsentHandler.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt @@ -1,10 +1,22 @@ -package org.microg.gms.asterism +@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.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.rcsFlowContext +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 @@ -12,23 +24,25 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.ByteString import okio.ByteString.Companion.toByteString -import org.microg.gms.constellation.AuthManager -import org.microg.gms.constellation.proto.AsterismClient -import org.microg.gms.constellation.proto.AsterismConsent -import org.microg.gms.constellation.proto.AuditToken -import org.microg.gms.constellation.proto.Consent -import org.microg.gms.constellation.proto.ConsentVersion -import org.microg.gms.constellation.proto.FlowContext -import org.microg.gms.constellation.proto.OnDemandConsent -import org.microg.gms.constellation.proto.Param -import org.microg.gms.constellation.proto.RcsConsent -import org.microg.gms.constellation.proto.RequestHeader -import org.microg.gms.constellation.proto.RequestTrigger -import org.microg.gms.constellation.proto.SetConsentRequest -import org.microg.gms.constellation.proto.builders.* -import org.microg.gms.asterism.rpc.builders.invoke -import org.microg.gms.constellation.ConstellationStateStore -import org.microg.gms.constellation.RpcClient +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.AuditToken +import org.microg.gms.constellation.core.proto.Consent +import org.microg.gms.constellation.core.proto.ConsentVersion +import org.microg.gms.constellation.core.proto.FlowContext +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" @@ -63,15 +77,7 @@ suspend fun handleSetAsterismConsent( return@withContext } - if (request.extras != null) { - Log.d(TAG, "Extras keys: ${request.extras.keySet().joinToString()}") - for (key in request.extras.keySet()) { - val value = request.extras.get(key) - Log.d(TAG, " $key = $value") - } - } - - if (request.status == SetAsterismConsentRequest.Status.ON_DEMAND) { + if (request.status == SetAsterismConsentRequestStatus.ON_DEMAND) { if (request.accountName.isNullOrBlank() && request.extras?.getString("gaia_access_token") .isNullOrBlank() ) { @@ -84,8 +90,7 @@ suspend fun handleSetAsterismConsent( } } - val extras = request.extras - val apiParams = Param.getList(extras) + val apiParams = Param.getList(request.extras) var deviceConsent: AsterismConsent? = null var rcsConsent: RcsConsent? = null @@ -100,8 +105,9 @@ suspend fun handleSetAsterismConsent( consent_source = request.deviceConsentSource ) } - request.status == SetAsterismConsentRequest.Status.ON_DEMAND -> { - onDemandConsent = OnDemandConsent(context, request, extras) + + request.status == SetAsterismConsentRequestStatus.ON_DEMAND -> { + onDemandConsent = OnDemandConsent(context, request, request.extras) if (onDemandConsent == null) { Log.e(TAG, "ODC Flow missing required fields, aborting.") callbacks.onConsentRegistered( @@ -111,6 +117,7 @@ suspend fun handleSetAsterismConsent( return@withContext } } + else -> { if (request.asterismClient == AsterismClient.CONSTELLATION) { if (request.language.isNullOrBlank() || request.consentVariant.isNullOrBlank()) { @@ -150,7 +157,7 @@ suspend fun handleSetAsterismConsent( AuditToken.generate().encode().toByteString() } else ByteString.EMPTY - val authManager = AuthManager.get(context) + val authManager = context.authManager val buildContext = buildRequestContext(context, authManager) val sessionId = UUID.randomUUID().toString() @@ -176,7 +183,10 @@ suspend fun handleSetAsterismConsent( if (e.grpcStatus == GrpcStatus.PERMISSION_DENIED || e.grpcStatus == GrpcStatus.UNAUTHENTICATED ) { - Log.w(TAG, "Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache...") + Log.w( + TAG, + "Suspicious client status ${e.grpcStatus.name}. Clearing DroidGuard cache..." + ) ConstellationStateStore.clearDroidGuardToken(context) } throw e @@ -193,7 +203,11 @@ suspend fun handleSetAsterismConsent( callbacks.onConsentRegistered( Status.SUCCESS, - SetAsterismConsentResponse(request.requestCode, buildContext.iidToken, authManager.getFid()) + SetAsterismConsentResponse( + request.requestCode, + buildContext.iidToken, + authManager.getFid() + ) ) } catch (e: Exception) { diff --git a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/proto/builders/OnDemandConsentBuilder.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/OnDemandConsentBuilder.kt similarity index 77% rename from play-services-asterism/src/main/kotlin/org/microg/gms/asterism/proto/builders/OnDemandConsentBuilder.kt rename to play-services-asterism/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/OnDemandConsentBuilder.kt index ae576343b8..3f501979bb 100644 --- a/play-services-asterism/src/main/kotlin/org/microg/gms/asterism/proto/builders/OnDemandConsentBuilder.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/OnDemandConsentBuilder.kt @@ -1,4 +1,4 @@ -package org.microg.gms.asterism.rpc.builders +package org.microg.gms.constellation.core.proto.builder import android.accounts.Account import android.accounts.AccountManager @@ -7,11 +7,14 @@ import android.content.Context import android.os.Bundle import android.util.Log import com.google.android.gms.asterism.SetAsterismConsentRequest -import org.microg.gms.constellation.proto.Consent -import org.microg.gms.constellation.proto.GaiaToken -import org.microg.gms.constellation.proto.OnDemandConsent +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 ODC_TAG = "OnDemandConsent" +private const val TAG = "OnDemandConsent" @SuppressLint("MissingPermission") operator fun OnDemandConsent.Companion.invoke( @@ -19,7 +22,7 @@ operator fun OnDemandConsent.Companion.invoke( request: SetAsterismConsentRequest, extras: Bundle? ): OnDemandConsent? { - val isOnDemand = request.status == SetAsterismConsentRequest.Status.ON_DEMAND || + val isOnDemand = request.status == SetAsterismConsentRequestStatus.ON_DEMAND || extras?.containsKey("consent_variant_key") == true || extras?.containsKey("consent_trigger_key") == true || extras?.containsKey("gaia_access_token") == true @@ -46,14 +49,14 @@ operator fun OnDemandConsent.Companion.invoke( ) future.result?.getString(AccountManager.KEY_AUTHTOKEN) } catch (e: Exception) { - Log.w(ODC_TAG, "Failed to get auth token for account $accountName: ${e.message}") + Log.w(TAG, "Failed to get auth token for account $accountName: ${e.message}") null } } } if (gaiaAccessToken.isNullOrBlank()) { - Log.w(ODC_TAG, "ODC missing gaiaAccessToken") + Log.w(TAG, "ODC missing gaiaAccessToken") return null } diff --git a/play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentRequest.java b/play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentRequest.java new file mode 100644 index 0000000000..e3c6be7f58 --- /dev/null +++ b/play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentRequest.java @@ -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 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); + } +} \ No newline at end of file diff --git a/play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentResponse.java b/play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentResponse.java new file mode 100644 index 0000000000..bef52f0fe2 --- /dev/null +++ b/play-services-asterism/src/main/java/com/google/android/gms/asterism/GetAsterismConsentResponse.java @@ -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 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); + } +} \ No newline at end of file diff --git a/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java new file mode 100644 index 0000000000..e049fbd518 --- /dev/null +++ b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java @@ -0,0 +1,133 @@ +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; + +@SafeParcelable.Class +public class SetAsterismConsentRequest extends AbstractSafeParcelable { + public static SafeParcelableCreatorAndWriter 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 tosUrl; + + @Field(10) + @Nullable + public final String language; + + @Field(11) + @Nullable + public final String country; + + @Field(12) + @Nullable + public final String tosVersion; + + @Field(13) + @Nullable + public final String tosContentTitle; + + @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 rcsFlowContextValue; + + @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 tosUrl, + @Param(10) @Nullable String language, + @Param(11) @Nullable String country, + @Param(12) @Nullable String tosVersion, + @Param(13) @Nullable String tosContentTitle, + @Param(14) @Nullable String accountName, + @Param(15) @Nullable String consentVariant, + @Param(16) @Nullable String consentTrigger, + @Param(17) int rcsFlowContextValue, + @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.tosUrl = tosUrl; + this.language = language; + this.country = country; + this.tosVersion = tosVersion; + this.tosContentTitle = tosContentTitle; + this.accountName = accountName; + this.consentVariant = consentVariant; + this.consentTrigger = consentTrigger; + this.rcsFlowContextValue = rcsFlowContextValue; + this.deviceConsentSourceValue = deviceConsentSourceValue; + this.deviceConsentVersionValue = deviceConsentVersionValue; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } +} \ No newline at end of file diff --git a/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentResponse.java b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentResponse.java new file mode 100644 index 0000000000..3fee3c251d --- /dev/null +++ b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentResponse.java @@ -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 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); + } +} \ No newline at end of file diff --git a/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt b/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt deleted file mode 100644 index d2f3139c4d..0000000000 --- a/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/GetAsterismConsent.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.google.android.gms.asterism - -import android.os.Parcel -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.SafeParcelable.Constructor -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param -import org.microg.gms.constellation.proto.AsterismClient -import org.microg.gms.constellation.proto.Consent -import org.microg.gms.constellation.proto.ConsentVersion - -@SafeParcelable.Class -data class GetAsterismConsentRequest @Constructor constructor( - @JvmField @Param(1) @Field(1) val requestCode: Int, - @JvmField @Param(2) @Field(2) val asterismClientValue: Int -) : AbstractSafeParcelable() { - val asterismClient: AsterismClient - get() = AsterismClient.fromValue(asterismClientValue) ?: AsterismClient.UNKNOWN_CLIENT - - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(GetAsterismConsentRequest::class.java) - } -} - -@SafeParcelable.Class -data class GetAsterismConsentResponse @Constructor constructor( - @JvmField @Param(1) @Field(1) val requestCode: Int, - @JvmField @Param(2) @Field(2) val consentStateValue: Int, - @JvmField @Param(3) @Field(3) val gmscoreIidToken: String?, - @JvmField @Param(4) @Field(4) val fid: String?, - @JvmField @Param(5) @Field(5) val consentTypeValue: Int -) : AbstractSafeParcelable() { - constructor( - requestCode: Int, - consentState: Consent, - gmscoreIidToken: String?, - fid: String?, - consentVersion: ConsentVersion - ) : this( - requestCode, - if (consentState == Consent.CONSENTED || consentState == Consent.CONSENT_UNKNOWN) consentState.value else Consent.NO_CONSENT.value, - gmscoreIidToken, - fid, - consentVersion.value - ) - - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(GetAsterismConsentResponse::class.java) - } -} diff --git a/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt b/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt deleted file mode 100644 index 93f8619690..0000000000 --- a/play-services-asterism/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.google.android.gms.asterism - -import android.os.Bundle -import android.os.Parcel -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.SafeParcelable.Constructor -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Field -import com.google.android.gms.common.internal.safeparcel.SafeParcelable.Param -import org.microg.gms.constellation.proto.AsterismClient -import org.microg.gms.constellation.proto.Consent -import org.microg.gms.constellation.proto.ConsentSource -import org.microg.gms.constellation.proto.ConsentVersion -import org.microg.gms.constellation.proto.FlowContext - -@SafeParcelable.Class -data class SetAsterismConsentRequest -@Constructor -constructor( - @JvmField @Param(1) @Field(1) val requestCode: Int, - @JvmField @Param(2) @Field(2) val asterismClientValue: Int, - @JvmField @Param(3) @Field(3) val flowContextValue: Int, - @JvmField @Param(4) @Field(value = 4, type = "int[]") val tosResourceIds: IntArray?, - @JvmField @Param(5) @Field(5) val timestamp: Long?, - @JvmField @Param(6) @Field(6) val consentValue: Int, - @JvmField @Param(7) @Field(7) val extras: Bundle?, - @JvmField @Param(8) @Field(8) val statusValue: Int, - @JvmField @Param(9) @Field(9) val tosUrl: String?, - @JvmField @Param(10) @Field(10) val language: String?, - @JvmField @Param(11) @Field(11) val country: String?, - @JvmField @Param(12) @Field(12) val tosVersion: String?, - @JvmField @Param(13) @Field(13) val tosContentTitle: String?, - @JvmField @Param(14) @Field(14) val accountName: String?, - @JvmField @Param(15) @Field(15) val consentVariant: String?, - @JvmField @Param(16) @Field(16) val consentTrigger: String?, - @JvmField @Param(17) @Field(17) val rcsFlowContextValue: Int, - @JvmField @Param(18) @Field(18) val deviceConsentSourceValue: Int, - @JvmField @Param(19) @Field(19) val deviceConsentVersionValue: Int, -) : AbstractSafeParcelable() { - enum class Status(val value: Int) { - RCS_DEFAULT(0), - RCS_LEGAL_FYI(1), - DEVICE_PNVR(2), - ON_DEMAND(3), - EXPIRED(4); - - companion object { - fun fromValue(value: Int): Status = entries.find { it.value == value } ?: EXPIRED - } - } - - val asterismClient: AsterismClient - get() = AsterismClient.fromValue(asterismClientValue) ?: AsterismClient.UNKNOWN_CLIENT - - val consent: Consent - get() = Consent.fromValue(consentValue) ?: Consent.CONSENT_UNKNOWN - - val rcsFlowContext: FlowContext - get() = FlowContext.fromValue(rcsFlowContextValue) ?: FlowContext.FLOW_CONTEXT_UNSPECIFIED - - val deviceConsentSource: ConsentSource - get() = ConsentSource.fromValue(deviceConsentSourceValue) - ?: ConsentSource.SOURCE_UNSPECIFIED - - val deviceConsentVersion: ConsentVersion - get() = ConsentVersion.fromValue(deviceConsentVersionValue).let { - if (it == null || it == ConsentVersion.CONSENT_VERSION_UNSPECIFIED) { - ConsentVersion.PHONE_VERIFICATION_DEFAULT - } else { - it - } - } - - fun isDevicePnvrFlow(): Boolean { - return asterismClient == AsterismClient.CONSTELLATION && - deviceConsentSourceValue > 0 && - deviceConsentVersionValue > 0 - } - - val status: Status - get() = Status.fromValue(statusValue) - - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(SetAsterismConsentRequest::class.java) - } -} - -@SafeParcelable.Class -data class SetAsterismConsentResponse -@Constructor -constructor( - @JvmField @Param(1) @Field(1) val requestCode: Int, - @JvmField @Param(2) @Field(2) val gmscoreIidToken: String?, - @JvmField @Param(3) @Field(3) val fid: String? -) : AbstractSafeParcelable() { - - override fun writeToParcel(out: Parcel, flags: Int) = CREATOR.writeToParcel(this, out, flags) - - companion object { - @JvmField - val CREATOR = findCreator(SetAsterismConsentResponse::class.java) - } -} diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 0b38b1058c..90db5e5ae7 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation project(':play-services-auth-api-phone-core') implementation project(':play-services-auth-blockstore-core') implementation project(':play-services-auth-workaccount-core') + implementation project(':play-services-asterism-core') implementation project(':play-services-base-core') implementation project(':play-services-cast-core') implementation project(':play-services-cast-framework-core') diff --git a/settings.gradle b/settings.gradle index ee0b621dda..51a7f47d8b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -88,6 +88,7 @@ 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' From 8e7c4deae4337802add1255e04a2c3f78bd64824 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 29 Mar 2026 06:44:22 -0400 Subject: [PATCH 44/63] Remove unnecessary dependencies --- play-services-asterism/core/build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/play-services-asterism/core/build.gradle b/play-services-asterism/core/build.gradle index eb06db5958..e37fe498f4 100644 --- a/play-services-asterism/core/build.gradle +++ b/play-services-asterism/core/build.gradle @@ -14,10 +14,6 @@ android { compileSdkVersion androidCompileSdk buildToolsVersion "$androidBuildVersionTools" - buildFeatures { - aidl = true - } - defaultConfig { versionName version minSdkVersion androidMinSdk @@ -47,8 +43,4 @@ dependencies { implementation project(':play-services-base-core') implementation project(':play-services-constellation-core') - implementation project(':play-services-iid') - - implementation project(':play-services-droidguard') - implementation project(':play-services-tasks-ktx') } From 99c9222e304edbdb7a2dce7ca7cb4f92d4863893 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:05:27 -0400 Subject: [PATCH 45/63] Update `RequestHeader` building --- .../gms/asterism/core/GetAsterismConsentHandler.kt | 9 ++++++++- .../gms/asterism/core/SetAsterismConsentHandler.kt | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/GetAsterismConsentHandler.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/GetAsterismConsentHandler.kt index 5c32952426..415c40d1ab 100644 --- a/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/GetAsterismConsentHandler.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/GetAsterismConsentHandler.kt @@ -21,6 +21,7 @@ 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 @@ -39,7 +40,13 @@ suspend fun handleGetAsterismConsent( val response = RpcClient.phoneDeviceVerificationClient.GetConsent().execute( GetConsentRequest( device_id = DeviceID(context, buildContext.iidToken), - header_ = RequestHeader(context, UUID.randomUUID().toString(), buildContext), + header_ = RequestHeader( + context, + UUID.randomUUID().toString(), + buildContext, + "getConsent", + RequestTrigger.Type.CONSENT_API_TRIGGER + ), asterism_client = request.asterismClient ) ) diff --git a/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt index b293ba2189..729c1a6080 100644 --- a/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt @@ -166,6 +166,7 @@ suspend fun handleSetAsterismConsent( context, sessionId, buildContext, + "setConsent", RequestTrigger.Type.CONSENT_API_TRIGGER ), asterism_client = request.asterismClient, From a457d82f9d783f8230bd85032288930c6585742d Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:31:43 -0400 Subject: [PATCH 46/63] Fix `SetAsterismConsent` handling --- .../gms/asterism/SetAsterismConsent.kt | 26 ++--- .../core/SetAsterismConsentHandler.kt | 103 +++++++++++------- .../proto/builder/OnDemandConsentBuilder.kt | 55 +++++----- .../asterism/SetAsterismConsentRequest.java | 36 +++--- .../gms/auth/AuthManagerServiceImpl.java | 3 - 5 files changed, 122 insertions(+), 101 deletions(-) diff --git a/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt b/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt index c51bf22095..23c464a7c6 100644 --- a/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt +++ b/play-services-asterism/core/src/main/kotlin/com/google/android/gms/asterism/SetAsterismConsent.kt @@ -1,21 +1,21 @@ 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 -import org.microg.gms.constellation.core.proto.FlowContext enum class SetAsterismConsentRequestStatus(val value: Int) { - RCS_DEFAULT(0), - RCS_LEGAL_FYI(1), - DEVICE_PNVR(2), + RESOURCE_TOS(0), + STATIC_STRING(1), + RCS(2), ON_DEMAND(3), - EXPIRED(4); + RESOURCE_TOS_FALLBACK(4); companion object { fun fromValue(value: Int): SetAsterismConsentRequestStatus = - entries.find { it.value == value } ?: EXPIRED + entries.find { it.value == value } ?: RESOURCE_TOS_FALLBACK } } @@ -25,16 +25,16 @@ val SetAsterismConsentRequest.asterismClient: AsterismClient val SetAsterismConsentRequest.consent: Consent get() = Consent.fromValue(consentValue) ?: Consent.CONSENT_UNKNOWN -val SetAsterismConsentRequest.rcsFlowContext: FlowContext - get() = FlowContext.fromValue(rcsFlowContextValue) ?: FlowContext.FLOW_CONTEXT_UNSPECIFIED +val SetAsterismConsentRequest.consentVersion: ConsentVersion? + get() = ConsentVersion.fromValue(consentVersionValue) val SetAsterismConsentRequest.deviceConsentSource: ConsentSource get() = ConsentSource.fromValue(deviceConsentSourceValue) ?: ConsentSource.SOURCE_UNSPECIFIED -val SetAsterismConsentRequest.deviceConsentVersion: ConsentVersion - get() = ConsentVersion.fromValue(deviceConsentVersionValue).let { - if (it == null || it == ConsentVersion.CONSENT_VERSION_UNSPECIFIED) { - ConsentVersion.PHONE_VERIFICATION_DEFAULT +val SetAsterismConsentRequest.deviceConsentVersion: DeviceConsentVersion + get() = DeviceConsentVersion.fromValue(deviceConsentVersionValue).let { + if (it == null || it == DeviceConsentVersion.UNKNOWN) { + DeviceConsentVersion.PHONE_VERIFICATION_DEFAULT } else { it } @@ -47,4 +47,4 @@ fun SetAsterismConsentRequest.isDevicePnvrFlow(): Boolean { return asterismClient == AsterismClient.CONSTELLATION && deviceConsentSourceValue > 0 && deviceConsentVersionValue > 0 -} \ No newline at end of file +} diff --git a/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt index 729c1a6080..5110ae2647 100644 --- a/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/asterism/core/SetAsterismConsentHandler.kt @@ -11,11 +11,11 @@ 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.rcsFlowContext import com.google.android.gms.asterism.status import com.google.android.gms.common.api.Status import com.squareup.wire.GrpcException @@ -29,10 +29,10 @@ 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.FlowContext import org.microg.gms.constellation.core.proto.OnDemandConsent import org.microg.gms.constellation.core.proto.Param import org.microg.gms.constellation.core.proto.RcsConsent @@ -62,14 +62,10 @@ suspend fun handleSetAsterismConsent( return@withContext } - val isRcsSpecificContext = request.rcsFlowContext in listOf( - FlowContext.FLOW_CONTEXT_RCS_CONSENT, - FlowContext.FLOW_CONTEXT_RCS_DEFAULT_ON_OUT_OF_BOX, - FlowContext.FLOW_CONTEXT_RCS_SAMSUNG_UNFREEZE - ) - - if (request.asterismClient != AsterismClient.RCS && isRcsSpecificContext) { - Log.e(TAG, "RCS-only flow context cannot be used with non-RCS client") + if (request.asterismClient != AsterismClient.CONSTELLATION && + request.asterismClient != AsterismClient.RCS + ) { + Log.e(TAG, "Invalid asterismClientValue: ${request.asterismClientValue}") callbacks.onConsentRegistered( Status.INTERNAL_ERROR, SetAsterismConsentResponse(request.requestCode, "", "") @@ -77,17 +73,25 @@ suspend fun handleSetAsterismConsent( return@withContext } - if (request.status == SetAsterismConsentRequestStatus.ON_DEMAND) { - if (request.accountName.isNullOrBlank() && request.extras?.getString("gaia_access_token") - .isNullOrBlank() - ) { - Log.e(TAG, "ODC missing accountName and no gaia_access_token in extras") - 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) @@ -95,7 +99,7 @@ suspend fun handleSetAsterismConsent( var deviceConsent: AsterismConsent? = null var rcsConsent: RcsConsent? = null var onDemandConsent: OnDemandConsent? = null - var flowContext: FlowContext = FlowContext.FLOW_CONTEXT_UNSPECIFIED + var consentVersion: ConsentVersion? = null when { request.isDevicePnvrFlow() -> { @@ -107,6 +111,24 @@ suspend fun handleSetAsterismConsent( } 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.") @@ -119,36 +141,43 @@ suspend fun handleSetAsterismConsent( } else -> { - if (request.asterismClient == AsterismClient.CONSTELLATION) { - if (request.language.isNullOrBlank() || request.consentVariant.isNullOrBlank()) { - Log.w( - TAG, - "CONSTELLATION client requires language and consentVariant" - ) - Log.w( - TAG, - " language=${request.language}, consentVariant=${request.consentVariant}" - ) + 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.RCS_OUT_OF_BOX + ConsentVersion.CONSENT_VERSION_UNSPECIFIED } rcsConsent = RcsConsent( consent = request.consent, consent_version = rcsConsentVersion ) - - flowContext = request.rcsFlowContext + consentVersion = request.consentVersion } } @@ -173,7 +202,7 @@ suspend fun handleSetAsterismConsent( device_consent = deviceConsent, rcs_consent = rcsConsent, on_demand_consent = onDemandConsent, - flow_context = flowContext, + consent_version = consentVersion ?: ConsentVersion.CONSENT_VERSION_UNSPECIFIED, api_params = apiParams, audit_record = auditRecordBytes ) @@ -193,7 +222,7 @@ suspend fun handleSetAsterismConsent( throw e } - if (deviceConsent?.consent_version == ConsentVersion.PHONE_VERIFICATION_REACHABILITY_INTL_SMS_CALLS) { + if (deviceConsent?.consent_version == DeviceConsentVersion.PHONE_VERIFICATION_REACHABILITY_INTL_SMS_CALLS) { ConstellationStateStore.storePnvrNotice( context, deviceConsent.consent, diff --git a/play-services-asterism/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/OnDemandConsentBuilder.kt b/play-services-asterism/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/OnDemandConsentBuilder.kt index 3f501979bb..77cd91b14d 100644 --- a/play-services-asterism/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/OnDemandConsentBuilder.kt +++ b/play-services-asterism/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/OnDemandConsentBuilder.kt @@ -22,37 +22,32 @@ operator fun OnDemandConsent.Companion.invoke( request: SetAsterismConsentRequest, extras: Bundle? ): OnDemandConsent? { - val isOnDemand = request.status == SetAsterismConsentRequestStatus.ON_DEMAND || - extras?.containsKey("consent_variant_key") == true || - extras?.containsKey("consent_trigger_key") == true || - extras?.containsKey("gaia_access_token") == true + if (request.status != SetAsterismConsentRequestStatus.ON_DEMAND) return null - if (!isOnDemand) return null + val consentVariant = request.consentVariant + val consentTrigger = request.consentTrigger + val accountName = request.accountName - val consentVariant = extras?.getString("consent_variant_key") - ?: request.consentVariant - - val consentTrigger = extras?.getString("consent_trigger_key") - ?: request.consentTrigger - - var gaiaAccessToken = extras?.getString("gaia_access_token") + if ( + accountName.isNullOrBlank() || + consentVariant.isNullOrBlank() || + consentTrigger.isNullOrBlank() + ) { + Log.w(TAG, "ODC missing required request fields") + return null + } - if (gaiaAccessToken.isNullOrBlank()) { - val accountName = request.accountName - if (!accountName.isNullOrBlank()) { - 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 - } - } + 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()) { @@ -69,7 +64,7 @@ operator fun OnDemandConsent.Companion.invoke( return OnDemandConsent( consent = consentValue, gaia_token = GaiaToken(token = gaiaAccessToken), - consent_variant = consentVariant ?: "", - trigger = consentTrigger ?: "" + consent_variant = consentVariant, + trigger = consentTrigger ) } diff --git a/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java index e049fbd518..ab507dc325 100644 --- a/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java +++ b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java @@ -44,7 +44,7 @@ public class SetAsterismConsentRequest extends AbstractSafeParcelable { @Field(9) @Nullable - public final String tosUrl; + public final String clientVersion; @Field(10) @Nullable @@ -52,15 +52,15 @@ public class SetAsterismConsentRequest extends AbstractSafeParcelable { @Field(11) @Nullable - public final String country; + public final String field11; @Field(12) @Nullable - public final String tosVersion; + public final String field12; @Field(13) @Nullable - public final String tosContentTitle; + public final String field13; @Field(14) @Nullable @@ -75,7 +75,7 @@ public class SetAsterismConsentRequest extends AbstractSafeParcelable { public final String consentTrigger; @Field(17) - public final int rcsFlowContextValue; + public final int consentVersionValue; @Field(18) public final int deviceConsentSourceValue; @@ -93,15 +93,15 @@ public SetAsterismConsentRequest( @Param(6) int consentValue, @Param(7) @Nullable Bundle extras, @Param(8) int statusValue, - @Param(9) @Nullable String tosUrl, - @Param(10) @Nullable String language, - @Param(11) @Nullable String country, - @Param(12) @Nullable String tosVersion, - @Param(13) @Nullable String tosContentTitle, + @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 rcsFlowContextValue, + @Param(17) int consentVersionValue, @Param(18) int deviceConsentSourceValue, @Param(19) int deviceConsentVersionValue ) { @@ -113,15 +113,15 @@ public SetAsterismConsentRequest( this.consentValue = consentValue; this.extras = extras; this.statusValue = statusValue; - this.tosUrl = tosUrl; - this.language = language; - this.country = country; - this.tosVersion = tosVersion; - this.tosContentTitle = tosContentTitle; + 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.rcsFlowContextValue = rcsFlowContextValue; + this.consentVersionValue = consentVersionValue; this.deviceConsentSourceValue = deviceConsentSourceValue; this.deviceConsentVersionValue = deviceConsentVersionValue; } @@ -130,4 +130,4 @@ public SetAsterismConsentRequest( public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } -} \ No newline at end of file +} diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java index 7563d49c43..e0ae41cc9e 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java @@ -240,10 +240,7 @@ public int hasCapabilities(HasCapabilitiesRequest request) throws RemoteExceptio PackageUtils.assertGooglePackagePermission(context, GooglePackagePermission.ACCOUNT); List services = Arrays.asList(AccountManager.get(context).getUserData(request.account, "services").split(",")); for (String capability : request.capabilities) { - // Required for com.google.android.apps.messaging to pass correct arguments to SetAsterismConsent - if (capability.equals("ge2tamrnmnqxa")) continue; if (capability.startsWith("service_") && !services.contains(capability.substring(8)) || !services.contains(capability)) { - Log.w(TAG, "Refusing, we do not have " + capability); return 6; } } From 46bab0c5cd425cbe06ce7303aae5d2d43910b78f Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:42:28 -0400 Subject: [PATCH 47/63] Add `toString()` for `SetAsterismConsentRequest` --- .../asterism/SetAsterismConsentRequest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java index ab507dc325..9e47805293 100644 --- a/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java +++ b/play-services-asterism/src/main/java/com/google/android/gms/asterism/SetAsterismConsentRequest.java @@ -10,6 +10,8 @@ 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 CREATOR = @@ -126,6 +128,32 @@ public SetAsterismConsentRequest( 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); From d2d0b6e01e4122d2cd4238f07eae78bf24eca535 Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:00:10 -0400 Subject: [PATCH 48/63] Remove unnecessary dependency --- play-services-core/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 90db5e5ae7..d22c89842e 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -60,7 +60,6 @@ dependencies { implementation project(':play-services-appinvite') implementation project(':play-services-auth-base') implementation project(':play-services-auth') - implementation project(':play-services-asterism') implementation project(':play-services-clearcut') implementation project(':play-services-drive') implementation project(':play-services-games') From 395864f0bf04db2f23a493d11cbd67ed52a105fa Mon Sep 17 00:00:00 2001 From: Opstic <46141527+opstic@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:38:50 -0400 Subject: [PATCH 49/63] Add phone number verification settings and usage record --- .../microg/gms/settings/SettingsContract.kt | 18 +++++ .../microg/gms/settings/SettingsProvider.kt | 28 ++++++++ play-services-constellation/core/build.gradle | 2 + .../core/ConstellationStateStore.kt | 44 ++++++++++++ .../constellation/core/VerifyPhoneNumber.kt | 19 +++++ .../PhoneNumberVerificationAppPreference.kt | 64 +++++++++++++++++ ...neNumberVerificationPreferencesFragment.kt | 61 ++++++++++++++++ .../core/src/main/res/drawable/ic_phone.xml | 16 +++++ ...nce_phone_number_verification_last_use.xml | 71 +++++++++++++++++++ .../core/src/main/res/values/strings.xml | 13 ++++ .../preferences_phone_number_verification.xml | 30 ++++++++ .../org/microg/gms/ui/SettingsFragment.kt | 13 ++++ .../src/main/res/navigation/nav_settings.xml | 8 +++ .../src/main/res/xml/preferences_start.xml | 4 ++ 14 files changed, 391 insertions(+) create mode 100644 play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationAppPreference.kt create mode 100644 play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt create mode 100644 play-services-constellation/core/src/main/res/drawable/ic_phone.xml create mode 100644 play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml create mode 100644 play-services-constellation/core/src/main/res/values/strings.xml create mode 100644 play-services-constellation/core/src/main/res/xml/preferences_phone_number_verification.xml diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index 5ef726412c..cf68d84f6f 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -220,6 +220,24 @@ object SettingsContract { ) } + object Constellation { + const val ID = "constellation" + fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID) + fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID" + + const val PHONE_NUMBER_VERIFICATION_ENABLED = "phone_number_verification_enabled" + const val PHONE_NUMBER_VERIFICATION_LAST_PACKAGE = "phone_number_verification_last_package" + const val PHONE_NUMBER_VERIFICATION_LAST_USED_AT_MS = "phone_number_verification_last_used_at_ms" + const val PHONE_NUMBER_VERIFICATION_LAST_SUCCESSFUL = "phone_number_verification_last_successful" + + val PROJECTION = arrayOf( + PHONE_NUMBER_VERIFICATION_ENABLED, + PHONE_NUMBER_VERIFICATION_LAST_PACKAGE, + PHONE_NUMBER_VERIFICATION_LAST_USED_AT_MS, + PHONE_NUMBER_VERIFICATION_LAST_SUCCESSFUL, + ) + } + object Profile { const val ID = "profile" fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index df0cabfd41..883cefe9fa 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -18,6 +18,7 @@ import android.preference.PreferenceManager import org.microg.gms.common.PackageUtils.warnIfNotMainProcess import org.microg.gms.settings.SettingsContract.Auth import org.microg.gms.settings.SettingsContract.CheckIn +import org.microg.gms.settings.SettingsContract.Constellation import org.microg.gms.settings.SettingsContract.DroidGuard import org.microg.gms.settings.SettingsContract.Exposure import org.microg.gms.settings.SettingsContract.GameProfile @@ -80,6 +81,7 @@ class SettingsProvider : ContentProvider() { Exposure.ID -> queryExposure(projection ?: Exposure.PROJECTION) SafetyNet.ID -> querySafetyNet(projection ?: SafetyNet.PROJECTION) DroidGuard.ID -> queryDroidGuard(projection ?: DroidGuard.PROJECTION) + Constellation.ID -> queryConstellation(projection ?: Constellation.PROJECTION) Profile.ID -> queryProfile(projection ?: Profile.PROJECTION) Location.ID -> queryLocation(projection ?: Location.PROJECTION) Vending.ID -> queryVending(projection ?: Vending.PROJECTION) @@ -103,6 +105,7 @@ class SettingsProvider : ContentProvider() { Exposure.ID -> updateExposure(values) SafetyNet.ID -> updateSafetyNet(values) DroidGuard.ID -> updateDroidGuard(values) + Constellation.ID -> updateConstellation(values) Profile.ID -> updateProfile(values) Location.ID -> updateLocation(values) Vending.ID -> updateVending(values) @@ -298,6 +301,31 @@ class SettingsProvider : ContentProvider() { editor.apply() } + private fun queryConstellation(p: Array): Cursor = MatrixCursor(p).addRow(p) { key -> + when (key) { + Constellation.PHONE_NUMBER_VERIFICATION_ENABLED -> getSettingsBoolean(key, false) + Constellation.PHONE_NUMBER_VERIFICATION_LAST_PACKAGE -> getSettingsString(key) + Constellation.PHONE_NUMBER_VERIFICATION_LAST_USED_AT_MS -> getSettingsLong(key, 0L) + Constellation.PHONE_NUMBER_VERIFICATION_LAST_SUCCESSFUL -> getSettingsBoolean(key, false) + else -> throw IllegalArgumentException("Unknown key: $key") + } + } + + private fun updateConstellation(values: ContentValues) { + if (values.size() == 0) return + val editor = preferences.edit() + values.valueSet().forEach { (key, value) -> + when (key) { + Constellation.PHONE_NUMBER_VERIFICATION_ENABLED -> editor.putBoolean(key, value as Boolean) + Constellation.PHONE_NUMBER_VERIFICATION_LAST_PACKAGE -> editor.putString(key, value as String?) + Constellation.PHONE_NUMBER_VERIFICATION_LAST_USED_AT_MS -> editor.putLong(key, value as Long) + Constellation.PHONE_NUMBER_VERIFICATION_LAST_SUCCESSFUL -> editor.putBoolean(key, value as Boolean) + else -> throw IllegalArgumentException("Unknown key: $key") + } + } + editor.apply() + } + private fun queryProfile(p: Array): Cursor = MatrixCursor(p).addRow(p) { key -> when (key) { Profile.PROFILE -> getSettingsString(key, "auto") diff --git a/play-services-constellation/core/build.gradle b/play-services-constellation/core/build.gradle index cd7df4f4db..6df901984a 100644 --- a/play-services-constellation/core/build.gradle +++ b/play-services-constellation/core/build.gradle @@ -59,4 +59,6 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" api "com.squareup.wire:wire-runtime:$wireVersion" api "com.squareup.wire:wire-grpc-client:$wireVersion" + + implementation "androidx.preference:preference-ktx:$preferenceVersion" } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt index 8f581309d2..259c784862 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt @@ -18,6 +18,8 @@ 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 +import org.microg.gms.settings.SettingsContract +import org.microg.gms.settings.SettingsContract.Constellation private const val STATE_PREFS_NAME = "constellation_prefs" private const val TOKEN_PREFS_NAME = "com.google.android.gms.constellation" @@ -30,6 +32,11 @@ 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" +data class PhoneNumberVerificationRecord( + val packageName: String, + val usedAtMillis: Long, + val successful: Boolean +) object ConstellationStateStore { fun loadVerificationTokens(context: Context): List { @@ -130,6 +137,43 @@ object ConstellationStateStore { } } + fun isPhoneNumberVerificationEnabled(context: Context): Boolean = + SettingsContract.getSettings( + context, + Constellation.getContentUri(context), + arrayOf(Constellation.PHONE_NUMBER_VERIFICATION_ENABLED) + ) { it.getInt(0) != 0 } + + fun setPhoneNumberVerificationEnabled(context: Context, enabled: Boolean) { + SettingsContract.setSettings(context, Constellation.getContentUri(context)) { + put(Constellation.PHONE_NUMBER_VERIFICATION_ENABLED, enabled) + } + } + + fun recordPhoneNumberVerification(context: Context, packageName: String, successful: Boolean) { + SettingsContract.setSettings(context, Constellation.getContentUri(context)) { + put(Constellation.PHONE_NUMBER_VERIFICATION_LAST_PACKAGE, packageName) + put(Constellation.PHONE_NUMBER_VERIFICATION_LAST_USED_AT_MS, System.currentTimeMillis()) + put(Constellation.PHONE_NUMBER_VERIFICATION_LAST_SUCCESSFUL, successful) + } + } + + fun loadLastPhoneNumberVerification(context: Context): PhoneNumberVerificationRecord? { + return SettingsContract.getSettings( + context, + Constellation.getContentUri(context), + Constellation.PROJECTION + ) { cursor -> + cursor.getString(1)?.let { packageName -> + PhoneNumberVerificationRecord( + packageName, + cursor.getLong(2), + cursor.getInt(3) != 0 + ) + } + } + } + private fun storeVerificationTokens(context: Context, tokens: List) { if (tokens.isEmpty()) return val filtered = tokens.filter { diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 6456cbb41b..07f97e53b6 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -176,6 +176,10 @@ private suspend fun handleVerifyPhoneNumberRequest( var phoneNumbers = emptyList() var verifications = emptyArray() val status = try { + if (!ConstellationStateStore.isPhoneNumberVerificationEnabled(context)) { + throw PhoneNumberVerificationDisabledException() + } + when (readCallbackMode) { ReadCallbackMode.LEGACY -> { Log.d(TAG, "Using read-only mode") @@ -205,6 +209,7 @@ private suspend fun handleVerifyPhoneNumberRequest( } catch (e: Exception) { Log.e(TAG, "verifyPhoneNumber failed", e) when { + e is PhoneNumberVerificationDisabledException -> Status(5000) readCallbackMode != ReadCallbackMode.NONE -> Status.INTERNAL_ERROR e is GrpcException -> handleRpcError(e) e is NoConsentException -> Status(5001) @@ -212,6 +217,18 @@ private suspend fun handleVerifyPhoneNumberRequest( } } + val successful = status.isSuccess && when (readCallbackMode) { + ReadCallbackMode.NONE -> verifications.isNotEmpty() && + (request.targetedSims.isEmpty() || + verifications.size == request.targetedSims.size) && + verifications.all { + it.verificationStatus == Verification.Status.STATUS_VERIFIED.toClientStatus() + } + + else -> true + } + ConstellationStateStore.recordPhoneNumberVerification(context, callingPackage, successful) + if (readCallbackMode == ReadCallbackMode.LEGACY || readCallbackMode == ReadCallbackMode.NONE && legacyCallbackOnFullFlow ) { @@ -232,6 +249,8 @@ private suspend fun handleVerifyPhoneNumberRequest( ) } +private class PhoneNumberVerificationDisabledException : Exception("Phone number verification is disabled") + private fun handleRpcError(error: GrpcException): Status { val statusCode = when (error.grpcStatus) { GrpcStatus.RESOURCE_EXHAUSTED -> 5008 diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationAppPreference.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationAppPreference.kt new file mode 100644 index 0000000000..e35d125eb7 --- /dev/null +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationAppPreference.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.ui + +import android.content.Context +import android.text.format.DateUtils +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import org.microg.gms.constellation.core.R +import org.microg.gms.ui.getApplicationInfoIfExists + +class PhoneNumberVerificationAppPreference : Preference { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + var packageName: String? = null + set(value) { + field = value + val packageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfoIfExists(value) + title = applicationInfo?.loadLabel(packageManager)?.toString() ?: value + icon = applicationInfo?.loadIcon(packageManager) + ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) + notifyChanged() + } + + var usedAtMillis: Long = 0L + set(value) { + field = value + notifyChanged() + } + + init { + isPersistent = false + layoutResource = R.layout.preference_phone_number_verification_last_use + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + val icon = holder.findViewById(android.R.id.icon) + if (icon is ImageView) { + icon.adjustViewBounds = true + icon.scaleType = ImageView.ScaleType.CENTER_INSIDE + icon.maxHeight = (32.0 * context.resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT).toInt() + } + holder.findViewById(R.id.phone_number_verification_last_use_time)?.let { + (it as TextView).text = if (usedAtMillis > 0L) { + DateUtils.getRelativeTimeSpanString(usedAtMillis) + } else { + null + } + } + } +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt new file mode 100644 index 0000000000..fd8185429f --- /dev/null +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.ui + +import android.os.Bundle +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import org.microg.gms.constellation.core.ConstellationStateStore +import org.microg.gms.constellation.core.R +import org.microg.gms.ui.SwitchBarPreference + +class PhoneNumberVerificationPreferencesFragment : PreferenceFragmentCompat() { + private lateinit var enabled: SwitchBarPreference + private lateinit var lastUseCategory: PreferenceCategory + private lateinit var app: PhoneNumberVerificationAppPreference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_phone_number_verification) + } + + override fun onBindPreferences() { + enabled = preferenceScreen.findPreference(PREF_ENABLED) ?: enabled + lastUseCategory = preferenceScreen.findPreference(PREF_LAST_USE_CATEGORY) ?: lastUseCategory + app = preferenceScreen.findPreference(PREF_LAST_USE) ?: app + enabled.setOnPreferenceChangeListener { _, newValue -> + ConstellationStateStore.setPhoneNumberVerificationEnabled( + requireContext(), + newValue as Boolean + ) + true + } + } + + override fun onResume() { + super.onResume() + enabled.isChecked = + ConstellationStateStore.isPhoneNumberVerificationEnabled(requireContext()) + val record = ConstellationStateStore.loadLastPhoneNumberVerification(requireContext()) + lastUseCategory.isVisible = record != null + if (record != null) { + app.packageName = record.packageName + app.summary = getString( + if (record.successful) { + R.string.phone_number_verification_result_success + } else { + R.string.phone_number_verification_result_failure + } + ) + app.usedAtMillis = record.usedAtMillis + } + } + + companion object { + private const val PREF_ENABLED = "pref_phone_number_verification_enabled" + private const val PREF_LAST_USE_CATEGORY = "prefcat_phone_number_verification_last_use" + private const val PREF_LAST_USE = "pref_phone_number_verification_last_use" + } +} diff --git a/play-services-constellation/core/src/main/res/drawable/ic_phone.xml b/play-services-constellation/core/src/main/res/drawable/ic_phone.xml new file mode 100644 index 0000000000..0d3944f5da --- /dev/null +++ b/play-services-constellation/core/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml b/play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml new file mode 100644 index 0000000000..ae89990e76 --- /dev/null +++ b/play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + diff --git a/play-services-constellation/core/src/main/res/values/strings.xml b/play-services-constellation/core/src/main/res/values/strings.xml new file mode 100644 index 0000000000..670fa8cb84 --- /dev/null +++ b/play-services-constellation/core/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + + Phone Number Verification + Enable phone number verification + Last use + Successful + Failed + Your carrier may charge you for texts and calls used for verification. + diff --git a/play-services-constellation/core/src/main/res/xml/preferences_phone_number_verification.xml b/play-services-constellation/core/src/main/res/xml/preferences_phone_number_verification.xml new file mode 100644 index 0000000000..db1b7547b8 --- /dev/null +++ b/play-services-constellation/core/src/main/res/xml/preferences_phone_number_verification.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt index 70335535ce..1226d9440f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt @@ -13,6 +13,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceCategory import com.google.android.gms.R import org.microg.gms.checkin.CheckinPreferences +import org.microg.gms.constellation.core.ConstellationStateStore import org.microg.gms.gcm.GcmDatabase import org.microg.gms.gcm.GcmPrefs import org.microg.gms.safetynet.SafetyNetPreferences @@ -42,6 +43,10 @@ class SettingsFragment : ResourceSettingsFragment() { findNavController().navigate(requireContext(), R.id.openSafetyNetSettings) true } + findPreference(PREF_PHONE_NUMBER_VERIFICATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openPhoneNumberVerificationSettings) + true + } findPreference(PREF_LOCATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { findNavController().navigate(requireContext(), R.id.openLocationSettings) true @@ -116,6 +121,13 @@ class SettingsFragment : ResourceSettingsFragment() { findPreference(PREF_CHECKIN)!!.setSummary(if (CheckinPreferences.isEnabled(requireContext())) org.microg.gms.base.core.R.string.service_status_enabled_short else org.microg.gms.base.core.R.string.service_status_disabled_short) findPreference(PREF_SNET)!!.setSummary(if (SafetyNetPreferences.isEnabled(requireContext())) org.microg.gms.base.core.R.string.service_status_enabled_short else org.microg.gms.base.core.R.string.service_status_disabled_short) + findPreference(PREF_PHONE_NUMBER_VERIFICATION)!!.setSummary( + if (ConstellationStateStore.isPhoneNumberVerificationEnabled(requireContext())) { + org.microg.gms.base.core.R.string.service_status_enabled_short + } else { + org.microg.gms.base.core.R.string.service_status_disabled_short + } + ) lifecycleScope.launchWhenResumed { val entries = getAllSettingsProviders(requireContext()).flatMap { it.getEntriesDynamic(requireContext()) } @@ -134,6 +146,7 @@ class SettingsFragment : ResourceSettingsFragment() { const val PREF_ABOUT = "pref_about" const val PREF_GCM = "pref_gcm" const val PREF_SNET = "pref_snet" + const val PREF_PHONE_NUMBER_VERIFICATION = "pref_phone_number_verification" const val PREF_LOCATION = "pref_location" const val PREF_CHECKIN = "pref_checkin" const val PREF_VENDING = "pref_vending" diff --git a/play-services-core/src/main/res/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml index 7aacdbf48f..fd8622e4ab 100644 --- a/play-services-core/src/main/res/navigation/nav_settings.xml +++ b/play-services-core/src/main/res/navigation/nav_settings.xml @@ -23,6 +23,9 @@ + @@ -163,6 +166,11 @@ app:argType="string" /> + + + Date: Sat, 1 Aug 2026 23:38:50 -0400 Subject: [PATCH 50/63] Auto-consent before phone verification --- .../constellation/core/VerifyPhoneNumber.kt | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 07f97e53b6..9154d33134 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -23,15 +23,20 @@ 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 @@ -212,7 +217,6 @@ private suspend fun handleVerifyPhoneNumberRequest( e is PhoneNumberVerificationDisabledException -> Status(5000) readCallbackMode != ReadCallbackMode.NONE -> Status.INTERNAL_ERROR e is GrpcException -> handleRpcError(e) - e is NoConsentException -> Status(5001) else -> Status.INTERNAL_ERROR } } @@ -264,8 +268,6 @@ private fun handleRpcError(error: GrpcException): Status { return Status(statusCode, error.message) } -private class NoConsentException : Exception("No consent") - private suspend fun runVerificationFlow( context: Context, request: VerifyPhoneNumberRequest, @@ -297,8 +299,24 @@ private suspend fun runVerificationFlow( } if (!consented) { - Log.e(TAG, "Consent has not been set. Not running verification.") - throw NoConsentException() + Log.e(TAG, "Consent has not been set. Auto-setting consent.") + val consentType = parseConsentVersion(request.extras) + 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) + } } } @@ -332,6 +350,14 @@ private suspend fun runVerificationFlow( return verifications } +private fun parseConsentVersion(extras: Bundle): ConsentVersion { + val value = extras.getString("consent_type") + val parsed = value?.toIntOrNull()?.let(ConsentVersion::fromValue) + ?: value?.let { runCatching { ConsentVersion.valueOf(it) }.getOrNull() } + return parsed?.takeUnless { it == ConsentVersion.CONSENT_VERSION_UNSPECIFIED } + ?: ConsentVersion.RCS_DEFAULT_ON_LEGAL_FYI +} + private fun PhoneNumberVerification.toLegacyPhoneNumberInfoOrNull(): PhoneNumberInfo? { if ( verificationStatus != Verification.Status.STATUS_VERIFIED.toClientStatus() || From c00ccb511030370cd408d31f05c2d3627aab6e91 Mon Sep 17 00:00:00 2001 From: nwinkelman2 Date: Wed, 2 Sep 2026 09:57:38 -0400 Subject: [PATCH 51/63] fix: make RCS modules pass current Android lint --- play-services-asterism/core/src/main/AndroidManifest.xml | 8 ++++++-- .../core/src/main/AndroidManifest.xml | 5 ++++- .../gms/constellation/core/ConstellationStateStore.kt | 8 ++++++-- .../ui/PhoneNumberVerificationPreferencesFragment.kt | 9 +++------ .../preference_phone_number_verification_last_use.xml | 6 ++++-- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/play-services-asterism/core/src/main/AndroidManifest.xml b/play-services-asterism/core/src/main/AndroidManifest.xml index 5031ca6820..9879f029f1 100644 --- a/play-services-asterism/core/src/main/AndroidManifest.xml +++ b/play-services-asterism/core/src/main/AndroidManifest.xml @@ -3,10 +3,14 @@ ~ SPDX-License-Identifier: Apache-2.0 --> - + - + diff --git a/play-services-constellation/core/src/main/AndroidManifest.xml b/play-services-constellation/core/src/main/AndroidManifest.xml index 4b9166754e..1695d4c2a4 100644 --- a/play-services-constellation/core/src/main/AndroidManifest.xml +++ b/play-services-constellation/core/src/main/AndroidManifest.xml @@ -27,7 +27,10 @@ - + diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt index 259c784862..f8ecc8916a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt @@ -1,5 +1,3 @@ -@file:RequiresApi(Build.VERSION_CODES.O) - package org.microg.gms.constellation.core import android.annotation.SuppressLint @@ -39,6 +37,7 @@ data class PhoneNumberVerificationRecord( ) object ConstellationStateStore { + @RequiresApi(Build.VERSION_CODES.O) fun loadVerificationTokens(context: Context): List { val prefs = tokenPrefs(context) val serialized = prefs.getString(KEY_VERIFICATION_TOKENS, null) ?: return emptyList() @@ -57,12 +56,14 @@ object ConstellationStateStore { } } + @RequiresApi(Build.VERSION_CODES.O) fun storeSyncResponse(context: Context, response: SyncResponse) { storeVerificationTokens(context, response.verification_tokens) storeDroidGuardToken(context, response.droidguard_token) storeNextSyncTime(context, response.next_sync_time) } + @RequiresApi(Build.VERSION_CODES.O) fun storeProceedResponse(context: Context, response: ProceedResponse) { storeDroidGuardToken(context, response.droidguard_token) storeNextSyncTime(context, response.next_sync_time) @@ -174,6 +175,7 @@ object ConstellationStateStore { } } + @RequiresApi(Build.VERSION_CODES.O) private fun storeVerificationTokens(context: Context, tokens: List) { if (tokens.isEmpty()) return val filtered = tokens.filter { @@ -189,6 +191,7 @@ object ConstellationStateStore { tokenPrefs(context).edit { putString(KEY_VERIFICATION_TOKENS, serialized) } } + @RequiresApi(Build.VERSION_CODES.O) private fun storeDroidGuardToken(context: Context, token: DroidguardToken?) { val tokenValue = token?.token?.takeIf { it.isNotEmpty() } ?: return val expiration = token.ttl?.toEpochMilli() ?: return @@ -199,6 +202,7 @@ object ConstellationStateStore { } } + @RequiresApi(Build.VERSION_CODES.O) private fun storeNextSyncTime(context: Context, timestamp: ServerTimestamp?) { val serverMillis = timestamp?.timestamp?.toEpochMilli() ?: return val localMillis = timestamp.now?.toEpochMilli() ?: return diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt index fd8185429f..54563fab6d 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ui/PhoneNumberVerificationPreferencesFragment.kt @@ -19,12 +19,9 @@ class PhoneNumberVerificationPreferencesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_phone_number_verification) - } - - override fun onBindPreferences() { - enabled = preferenceScreen.findPreference(PREF_ENABLED) ?: enabled - lastUseCategory = preferenceScreen.findPreference(PREF_LAST_USE_CATEGORY) ?: lastUseCategory - app = preferenceScreen.findPreference(PREF_LAST_USE) ?: app + enabled = requireNotNull(findPreference(PREF_ENABLED)) + lastUseCategory = requireNotNull(findPreference(PREF_LAST_USE_CATEGORY)) + app = requireNotNull(findPreference(PREF_LAST_USE)) enabled.setOnPreferenceChangeListener { _, newValue -> ConstellationStateStore.setPhoneNumberVerificationEnabled( requireContext(), diff --git a/play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml b/play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml index ae89990e76..e278c50a13 100644 --- a/play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml +++ b/play-services-constellation/core/src/main/res/layout/preference_phone_number_verification_last_use.xml @@ -21,6 +21,7 @@ android:gravity="start|center_vertical" android:minWidth="56dp" android:orientation="horizontal" + android:paddingStart="12dp" android:paddingTop="4dp" android:paddingEnd="12dp" android:paddingBottom="4dp"> @@ -29,6 +30,7 @@ android:id="@android:id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@null" android:maxWidth="48dp" android:maxHeight="48dp" /> @@ -55,7 +57,7 @@ android:layout_below="@android:id/title" android:layout_alignStart="@android:id/title" android:maxLines="2" - android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textAppearance="?attr/textAppearanceListItemSecondary" android:textColor="?android:attr/textColorSecondary" /> @@ -66,6 +68,6 @@ android:layout_marginStart="12dp" android:gravity="end|center_vertical" android:maxLines="1" - android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textAppearance="?attr/textAppearanceListItemSecondary" android:textColor="?android:attr/textColorSecondary" /> From e89740d0684379f6dc8542f386b3b5aaa582c811 Mon Sep 17 00:00:00 2001 From: Camilo Cabezas Date: Thu, 20 Aug 2026 08:46:14 -0500 Subject: [PATCH 52/63] Fix invalid IID handling in Constellation GPNV InstanceID failures could be suppressed into an empty string. The empty credential was then exposed as a successful getIidToken response, signed, and included in a GetVerifiedPhoneNumbers request. Propagate InstanceID failures to the existing error handlers and reject empty credentials before reading the FID, signing, building IIDTokenAuth, or executing GPNV. Regression tests cover exceptions, empty credentials, and the valid request path. PR #3388 handles IID failures in a different Constellation implementation. This change applies equivalent error semantics plus explicit empty-token and GPNV guards to the #3359 architecture. --- play-services-constellation/core/build.gradle | 5 + .../gms/constellation/core/AuthManagerTest.kt | 32 ++++++ .../gms/constellation/core/GetIidTokenTest.kt | 107 +++++++++++++++++ .../core/GetVerifiedPhoneNumbersTest.kt | 108 ++++++++++++++++++ .../gms/constellation/core/AuthManager.kt | 10 +- .../gms/constellation/core/GetIidToken.kt | 36 +++++- .../core/GetVerifiedPhoneNumbers.kt | 23 ++-- 7 files changed, 303 insertions(+), 18 deletions(-) create mode 100644 play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerTest.kt create mode 100644 play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetIidTokenTest.kt create mode 100644 play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbersTest.kt diff --git a/play-services-constellation/core/build.gradle b/play-services-constellation/core/build.gradle index 6df901984a..d04442c2e9 100644 --- a/play-services-constellation/core/build.gradle +++ b/play-services-constellation/core/build.gradle @@ -19,6 +19,8 @@ android { versionName version minSdkVersion androidMinSdk targetSdkVersion androidTargetSdk + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true } sourceSets { @@ -61,4 +63,7 @@ dependencies { api "com.squareup.wire:wire-grpc-client:$wireVersion" implementation "androidx.preference:preference-ktx:$preferenceVersion" + + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestImplementation 'junit:junit:4.13.2' } diff --git a/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerTest.kt b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerTest.kt new file mode 100644 index 0000000000..1f8c8211a5 --- /dev/null +++ b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerTest.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core + +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.IOException + +class AuthManagerTest { + @Test + fun iidFailureIsNotConvertedToEmptyToken() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val authManager = AuthManager.get(instrumentation.targetContext) + var failure: Throwable? = null + + instrumentation.runOnMainSync { + try { + authManager.getIidToken("test-sender") + } catch (throwable: Throwable) { + failure = throwable + } + } + + assertNotNull("InstanceID MAIN_THREAD failure was suppressed", failure) + assertTrue("Expected IOException, got ${failure?.javaClass?.name}", failure is IOException) + } +} diff --git a/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetIidTokenTest.kt b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetIidTokenTest.kt new file mode 100644 index 0000000000..a02e3efcd4 --- /dev/null +++ b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetIidTokenTest.kt @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core + +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.GetPnvCapabilitiesResponse +import com.google.android.gms.constellation.PhoneNumberInfo +import com.google.android.gms.constellation.VerifyPhoneNumberResponse +import com.google.android.gms.constellation.internal.IConstellationCallbacks +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import java.io.IOException + +class GetIidTokenTest { + @Test + fun validTokenReturnsSuccess() = runBlocking { + val callbacks = RecordingCallbacks() + + handleGetIidToken(callbacks, GetIidTokenRequest(null), provider { "iid-token" }) + + assertEquals(Status.SUCCESS.statusCode, callbacks.status?.statusCode) + assertEquals("iid-token", callbacks.response?.iidToken) + assertEquals("fid", callbacks.response?.fid) + assertArrayEquals(byteArrayOf(1, 2, 3), callbacks.response?.signature) + assertEquals(1234L, callbacks.response?.timestamp) + } + + @Test + fun tokenFailureReturnsInternalError() = runBlocking { + val callbacks = RecordingCallbacks() + + handleGetIidToken(callbacks, GetIidTokenRequest(null), provider { + throw IOException("test failure") + }) + + assertEquals(Status.INTERNAL_ERROR.statusCode, callbacks.status?.statusCode) + assertNull(callbacks.response) + } + + @Test + fun emptyTokenReturnsInternalError() = runBlocking { + val callbacks = RecordingCallbacks() + var tokenWasSigned = false + + val provider = object : IidCredentialProvider { + override fun getIidToken(projectNumber: String?): String = "" + override fun getFid(): String = "fid" + override fun signIidToken(iidToken: String): Pair { + tokenWasSigned = true + return byteArrayOf(1, 2, 3) to 1234L + } + } + handleGetIidToken(callbacks, GetIidTokenRequest(null), provider) + + assertEquals(Status.INTERNAL_ERROR.statusCode, callbacks.status?.statusCode) + assertNull(callbacks.response) + assertEquals(false, tokenWasSigned) + } + + private fun provider(token: () -> String) = object : IidCredentialProvider { + override fun getIidToken(projectNumber: String?): String = token() + override fun getFid(): String = "fid" + override fun signIidToken(iidToken: String): Pair = + byteArrayOf(1, 2, 3) to 1234L + } + + private class RecordingCallbacks : IConstellationCallbacks.Stub() { + var status: Status? = null + var response: GetIidTokenResponse? = null + + override fun onIidTokenGenerated( + status: Status?, + response: GetIidTokenResponse?, + apiMetadata: ApiMetadata? + ) { + this.status = status + this.response = response + } + + override fun onPhoneNumberVerified( + status: Status?, + phoneNumbers: List?, + apiMetadata: ApiMetadata? + ) = Unit + + override fun onPhoneNumberVerificationsCompleted( + status: Status?, + response: VerifyPhoneNumberResponse?, + apiMetadata: ApiMetadata? + ) = Unit + + override fun onGetPnvCapabilitiesCompleted( + status: Status?, + response: GetPnvCapabilitiesResponse?, + apiMetadata: ApiMetadata? + ) = Unit + } +} diff --git a/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbersTest.kt b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbersTest.kt new file mode 100644 index 0000000000..e37d34def6 --- /dev/null +++ b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbersTest.kt @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core + +import android.os.Bundle +import androidx.test.platform.app.InstrumentationRegistry +import com.squareup.wire.GrpcException +import com.squareup.wire.GrpcStatus +import com.squareup.wire.Instant +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.fail +import org.junit.Test +import org.microg.gms.constellation.core.proto.GetVerifiedPhoneNumbersRequest +import org.microg.gms.constellation.core.proto.GetVerifiedPhoneNumbersResponse +import org.microg.gms.constellation.core.proto.IIDTokenAuth +import java.io.IOException + +class GetVerifiedPhoneNumbersTest { + @Test + fun invalidIidIsRejectedBeforeGpnvRequestIsSent() = runBlocking { + val backend = RejectEmptyIidBackend() + + try { + backend.execute( + GetVerifiedPhoneNumbersRequest( + iid_token_auth = IIDTokenAuth(iid_token = "") + ) + ) + fail("The mock backend accepted an empty IID token") + } catch (e: GrpcException) { + assertEquals(GrpcStatus.INVALID_ARGUMENT, e.grpcStatus) + } + assertEquals(1, backend.callCount) + + backend.callCount = 0 + var tokenWasSigned = false + try { + fetchVerifiedPhoneNumbers( + context = InstrumentationRegistry.getInstrumentation().targetContext, + bundle = Bundle(), + getIidToken = { "" }, + signIidToken = { + tokenWasSigned = true + byteArrayOf(1) to Instant.ofEpochMilli(1) + }, + execute = backend::execute + ) + fail("An empty IID token reached GPNV") + } catch (_: IllegalArgumentException) { + } + + assertFalse(tokenWasSigned) + assertEquals(0, backend.callCount) + } + + @Test + fun iidFailureDoesNotSendGpnvRequest() = runBlocking { + val backend = RejectEmptyIidBackend() + + try { + fetchVerifiedPhoneNumbers( + context = InstrumentationRegistry.getInstrumentation().targetContext, + bundle = Bundle(), + getIidToken = { throw IOException("test failure") }, + execute = backend::execute + ) + fail("An IID failure reached GPNV") + } catch (_: IOException) { + } + + assertEquals(0, backend.callCount) + } + + @Test + fun validIidSendsGpnvRequest() = runBlocking { + val backend = RejectEmptyIidBackend() + + fetchVerifiedPhoneNumbers( + context = InstrumentationRegistry.getInstrumentation().targetContext, + bundle = Bundle(), + getIidToken = { "iid-token" }, + signIidToken = { byteArrayOf(1) to Instant.ofEpochMilli(1) }, + execute = backend::execute + ) + + assertEquals(1, backend.callCount) + assertEquals("iid-token", backend.lastRequest?.iid_token_auth?.iid_token) + } + + private class RejectEmptyIidBackend { + var callCount = 0 + var lastRequest: GetVerifiedPhoneNumbersRequest? = null + + suspend fun execute(request: GetVerifiedPhoneNumbersRequest): GetVerifiedPhoneNumbersResponse { + callCount++ + lastRequest = request + if (request.iid_token_auth?.iid_token.isNullOrEmpty()) { + throw GrpcException(GrpcStatus.INVALID_ARGUMENT, "invalid IID", null) + } + return GetVerifiedPhoneNumbersResponse() + } + } +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt index 6f2a151427..bf26abaa9a 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt @@ -55,12 +55,8 @@ class AuthManager private constructor(context: Context) { } fun getIidToken(projectNumber: String? = null): String { - return try { - val sender = projectNumber ?: IidTokenPhenotypes.DEFAULT_PROJECT_NUMBER - InstanceID.getInstance(context).getToken(sender, "GCM") - } catch (_: Exception) { - "" - } + val sender = projectNumber ?: IidTokenPhenotypes.DEFAULT_PROJECT_NUMBER + return InstanceID.getInstance(context).getToken(sender, "GCM") } fun getOrCreateKeyPair(): KeyPair { @@ -117,4 +113,4 @@ class AuthManager private constructor(context: Context) { } fun getFid(): String = InstanceID.getInstance(context).id -} \ No newline at end of file +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt index 5ee8a6ac71..47099ce3fc 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetIidToken.kt @@ -12,16 +12,44 @@ import kotlinx.coroutines.withContext private const val TAG = "GetIidToken" +internal interface IidCredentialProvider { + fun getIidToken(projectNumber: String?): String + fun getFid(): String + fun signIidToken(iidToken: String): Pair +} + +private class AuthManagerIidCredentialProvider( + private val authManager: AuthManager +) : IidCredentialProvider { + override fun getIidToken(projectNumber: String?): String = + authManager.getIidToken(projectNumber) + + override fun getFid(): String = authManager.getFid() + + override fun signIidToken(iidToken: String): Pair = + authManager.signIidTokenCompat(iidToken) +} + suspend fun handleGetIidToken( context: Context, callbacks: IConstellationCallbacks, request: GetIidTokenRequest +) = handleGetIidToken( + callbacks, + request, + AuthManagerIidCredentialProvider(context.authManager) +) + +internal suspend fun handleGetIidToken( + callbacks: IConstellationCallbacks, + request: GetIidTokenRequest, + credentialProvider: IidCredentialProvider ) = 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) + val iidToken = credentialProvider.getIidToken(request.projectNumber?.toString()) + require(iidToken.isNotEmpty()) { "Instance ID token is empty" } + val fid = credentialProvider.getFid() + val (signature, timestamp) = credentialProvider.signIidToken(iidToken) callbacks.onIidTokenGenerated( Status.SUCCESS, diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt index cd63473dd9..18c1c0e36f 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/GetVerifiedPhoneNumbers.kt @@ -8,11 +8,13 @@ import android.os.Bundle import android.util.Log import com.google.android.gms.constellation.PhoneNumberInfo import com.google.android.gms.constellation.VerifyPhoneNumberResponse.PhoneNumberVerification +import com.squareup.wire.Instant 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.GetVerifiedPhoneNumbersResponse 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 @@ -24,17 +26,26 @@ private const val TAG = "GetVerifiedPhoneNumbers" internal suspend fun fetchVerifiedPhoneNumbers( context: Context, bundle: Bundle, - callingPackage: String = bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME + callingPackage: String = bundle.getString("calling_package") ?: Constants.GMS_PACKAGE_NAME, + getIidToken: () -> String = { + context.authManager.getIidToken(IidTokenPhenotypes.READ_ONLY_PROJECT_NUMBER) + }, + signIidToken: (String) -> Pair = { + context.authManager.signIidToken(it) + }, + execute: suspend (GetVerifiedPhoneNumbersRequest) -> GetVerifiedPhoneNumbersResponse = { + RpcClient.phoneNumberClient.GetVerifiedPhoneNumbers().execute(it) + } ): List = 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 iidToken = getIidToken() + require(iidToken.isNotEmpty()) { "Instance ID token is empty" } val iidTokenAuth = if (VerifyPhoneNumberApiPhenotypes.ENABLE_CLIENT_SIGNATURE) { - val (signatureBytes, signTimestamp) = authManager.signIidToken(iidToken) + val (signatureBytes, signTimestamp) = signIidToken(iidToken) IIDTokenAuth( iid_token = iidToken, client_sign = signatureBytes.toByteString(), @@ -56,9 +67,7 @@ internal suspend fun fetchVerifiedPhoneNumbers( ) Log.d(TAG, "Calling GetVerifiedPhoneNumbers RPC (read-only mode)...") - val response = RpcClient.phoneNumberClient - .GetVerifiedPhoneNumbers() - .execute(getRequest) + val response = execute(getRequest) Log.d(TAG, "GetVerifiedPhoneNumbers response: ${response.phone_numbers.size} numbers") response.phone_numbers } From 719a221478e95557dfcba2bed36309a7c8e463ff Mon Sep 17 00:00:00 2001 From: Paulcake Date: Sun, 23 Aug 2026 02:16:31 +0100 Subject: [PATCH 53/63] Fix stale Constellation key acknowledgement A regenerated EC key was allowed to inherit the previous key's server acknowledgement. Validate stored key material before reading that state, and reset it atomically whenever key material is generated. Add instrumentation coverage for corrupt and valid stored key pairs. --- .../core/AuthManagerKeyAcknowledgementTest.kt | 59 +++++++++++++++++++ .../gms/constellation/core/AuthManager.kt | 2 + .../core/ConstellationStateStore.kt | 2 + 3 files changed, 63 insertions(+) create mode 100644 play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerKeyAcknowledgementTest.kt diff --git a/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerKeyAcknowledgementTest.kt b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerKeyAcknowledgementTest.kt new file mode 100644 index 0000000000..a3513d76f6 --- /dev/null +++ b/play-services-constellation/core/src/androidTest/kotlin/org/microg/gms/constellation/core/AuthManagerKeyAcknowledgementTest.kt @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core + +import android.content.Context +import android.content.SharedPreferences +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class AuthManagerKeyAcknowledgementTest { + private lateinit var context: Context + private lateinit var preferences: SharedPreferences + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + assertTrue(preferences.edit().clear().commit()) + } + + @After + fun tearDown() { + preferences.edit().clear().commit() + } + + @Test + fun validKeyPairRetainsServerAcknowledgement() { + context.authManager.getOrCreateKeyPair() + assertTrue(preferences.edit().putBoolean(KEY_PUBLIC_KEY_ACKED, true).commit()) + + assertTrue(ConstellationStateStore.isPublicKeyAcked(context)) + } + + @Test + fun corruptKeyPairIsNotTreatedAsServerAcknowledged() { + context.authManager.getOrCreateKeyPair() + assertTrue( + preferences.edit() + .putString(KEY_PRIVATE, "not-a-private-key") + .putBoolean(KEY_PUBLIC_KEY_ACKED, true) + .commit() + ) + + assertFalse(ConstellationStateStore.isPublicKeyAcked(context)) + } + + private companion object { + const val PREFS_NAME = "constellation_prefs" + const val KEY_PRIVATE = "private_key" + const val KEY_PUBLIC_KEY_ACKED = "is_public_key_acked" + } +} diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt index bf26abaa9a..c27b72fcf2 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/AuthManager.kt @@ -26,6 +26,7 @@ class AuthManager private constructor(context: Context) { private const val PREFS_NAME = "constellation_prefs" private const val KEY_PRIVATE = "private_key" private const val KEY_PUBLIC = "public_key" + private const val KEY_PUBLIC_KEY_ACKED = "is_public_key_acked" // This is safe as the Context is immediately converted to the application context. @SuppressLint("StaticFieldLeak") @@ -95,6 +96,7 @@ class AuthManager private constructor(context: Context) { sharedPrefs.edit { putString(KEY_PRIVATE, Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)) putString(KEY_PUBLIC, Base64.encodeToString(kp.public.encoded, Base64.NO_WRAP)) + putBoolean(KEY_PUBLIC_KEY_ACKED, false) } return kp diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt index f8ecc8916a..f40bc69c81 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/ConstellationStateStore.kt @@ -110,6 +110,8 @@ object ConstellationStateStore { } fun isPublicKeyAcked(context: Context): Boolean { + // Regeneration invalidates acknowledgement before it is read. + context.authManager.getOrCreateKeyPair() return statePrefs(context).getBoolean(KEY_PUBLIC_KEY_ACKED, false) } From fd27912ad344a1f2a7ab24c867bbcee66a37c5fe Mon Sep 17 00:00:00 2001 From: nwinkelman2 Date: Wed, 2 Sep 2026 10:22:46 -0400 Subject: [PATCH 54/63] test: keep Constellation instrumentation compatible with current master --- .../core/src/androidTest/AndroidManifest.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 play-services-constellation/core/src/androidTest/AndroidManifest.xml diff --git a/play-services-constellation/core/src/androidTest/AndroidManifest.xml b/play-services-constellation/core/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..4eab3f9bed --- /dev/null +++ b/play-services-constellation/core/src/androidTest/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + From 3e0509c57f774b5cb64c949ab80e1ebbb32678fd Mon Sep 17 00:00:00 2001 From: "DESKTOP-691E36A\\Administrator" Date: Wed, 15 Jul 2026 09:29:05 -0600 Subject: [PATCH 55/63] Fix DroidGuard tachyon parity and RCS phone-number fallback. Align DG cache naming and VM keys with stock GMS, preserve partial init replies, and fall back to phone number hints during Constellation provisioning. --- .../core/proto/builder/SyncRequestBuilder.kt | 9 +++++++- .../core/NetworkHandleProxyFactory.kt | 2 +- .../internal/DroidGuardInitReply.java | 6 +++--- .../microg/gms/droidguard/DgVmClassLoader.kt | 21 +++++++++++++++++++ .../gms/droidguard/HandleProxyFactory.kt | 6 +++--- 5 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt index 1c675a0875..c80d464b47 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt @@ -161,7 +161,14 @@ suspend operator fun SyncRequest.Companion.invoke( val verifications = imsiToInfoMap.map { (imsi, subscriptionInfo) -> val subscriptionId = subscriptionInfo.subscriptionId val slotIndex = subscriptionInfo.simSlotIndex - val phoneNumber = PhoneNumberUtils.formatNumberToE164(subscriptionInfo.number, subscriptionInfo.countryIso) ?: "" + val phoneNumberHint = request.targetedSims + .firstOrNull { it.imsi == imsi } + ?.phoneNumberHint + ?.takeIf { it.isNotBlank() } + val phoneNumber = PhoneNumberUtils.formatNumberToE164( + subscriptionInfo.number, + subscriptionInfo.countryIso + ) ?: phoneNumberHint ?: subscriptionInfo.number.orEmpty() val iccid = subscriptionInfo.iccId ?: "" Verification( diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt index 4e685de383..fc009e8b75 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt @@ -135,7 +135,7 @@ class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFacto }) val signed: SignedResponse = future.get() val response = signed.unpack() - val vmKey = response.vmChecksum!!.hex() + val vmKey = response.vmChecksum!!.hex().uppercase(Locale.US) if (!isValidCache(vmKey)) { val temp = File(getCacheDir(), "${UUID.randomUUID()}.apk") temp.parentFile!!.mkdirs() diff --git a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java index 0294a5c0e8..fb5f8e5bfc 100644 --- a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java +++ b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java @@ -36,10 +36,10 @@ public void writeToParcel(Parcel dest, int flags) { public DroidGuardInitReply createFromParcel(Parcel source) { ParcelFileDescriptor pfd = source.readParcelable(ParcelFileDescriptor.class.getClassLoader()); Parcelable object = source.readParcelable(getClass().getClassLoader()); - if (pfd != null && object != null) { - return new DroidGuardInitReply(pfd, object); + if (pfd == null && object == null) { + return null; } - return null; + return new DroidGuardInitReply(pfd, object); } @Override diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt new file mode 100644 index 0000000000..f0b6461cff --- /dev/null +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard + +import dalvik.system.DexClassLoader + +/** + * Named DexClassLoader for DroidGuard VM bytecode. + * + * Anonymous DexClassLoader instances leak "org.microg" in class names visible to + * DroidGuard's /proc/self/maps inspection, causing tachyon_registration failures. + */ +class DgVmClassLoader( + dexPath: String, + optimizedDirectory: String, + librarySearchPath: String?, + parent: ClassLoader +) : DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt index 0ae6ccfbdb..b1d0d7708e 100644 --- a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt @@ -10,7 +10,7 @@ import android.os.Bundle import android.os.ParcelFileDescriptor import android.os.Parcelable import androidx.annotation.GuardedBy -import dalvik.system.DexClassLoader +import org.microg.gms.droidguard.DgVmClassLoader import java.io.File import java.io.IOException import java.security.MessageDigest @@ -89,7 +89,7 @@ open class HandleProxyFactory(private val context: Context) { getCacheDir(vmKey).deleteRecursively() throw ClassNotFoundException("APK signature verification failed") } - val loader = DexClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) + val loader = DgVmClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) val clazz = loader.loadClass(CLASS_NAME) CLASS_MAP[vmKey] = clazz return clazz @@ -98,7 +98,7 @@ open class HandleProxyFactory(private val context: Context) { companion object { const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" - const val CACHE_FOLDER_NAME = "cache_dg" + const val CACHE_FOLDER_NAME = "dg_cache" private val CLASS_MAP = hashMapOf>() val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) } From 150714ad800fa2ac2f1678297531fb1aa6c86c10 Mon Sep 17 00:00:00 2001 From: "DESKTOP-691E36A\\Administrator" Date: Wed, 15 Jul 2026 09:31:29 -0600 Subject: [PATCH 56/63] Add RCS unit tests and extract testable DroidGuard helpers. Introduce shared phone-number and VM cache utilities, wire them into Constellation and DroidGuard, and add offline JUnit coverage for provisioning and tachyon-related logic. --- play-services-constellation/core/build.gradle | 7 ++ .../core/proto/builder/SyncRequestBuilder.kt | 13 ++-- .../builder/SyncRequestPhoneNumberResolver.kt | 26 +++++++ .../SyncRequestPhoneNumberResolverTest.kt | 72 +++++++++++++++++++ .../core/verification/ts43/Fips186PrfTest.kt | 53 ++++++++++++++ play-services-droidguard/build.gradle | 7 ++ .../core/NetworkHandleProxyFactory.kt | 2 +- .../gms/droidguard/DroidGuardVmCache.kt | 17 +++++ .../gms/droidguard/HandleProxyFactory.kt | 6 +- .../internal/DroidGuardInitReplyTest.java | 67 +++++++++++++++++ .../gms/droidguard/DgVmClassLoaderTest.kt | 19 +++++ .../gms/droidguard/DroidGuardVmCacheTest.kt | 33 +++++++++ 12 files changed, 313 insertions(+), 9 deletions(-) create mode 100644 play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolver.kt create mode 100644 play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolverTest.kt create mode 100644 play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/Fips186PrfTest.kt create mode 100644 play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DroidGuardVmCache.kt create mode 100644 play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java create mode 100644 play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt create mode 100644 play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DroidGuardVmCacheTest.kt diff --git a/play-services-constellation/core/build.gradle b/play-services-constellation/core/build.gradle index d04442c2e9..348cd202cf 100644 --- a/play-services-constellation/core/build.gradle +++ b/play-services-constellation/core/build.gradle @@ -25,6 +25,11 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + testOptions { + unitTests.returnDefaultValues = true } compileOptions { @@ -64,6 +69,8 @@ dependencies { implementation "androidx.preference:preference-ktx:$preferenceVersion" + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.6.2' androidTestImplementation 'junit:junit:4.13.2' } diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt index c80d464b47..5ee863406f 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestBuilder.kt @@ -164,11 +164,14 @@ suspend operator fun SyncRequest.Companion.invoke( val phoneNumberHint = request.targetedSims .firstOrNull { it.imsi == imsi } ?.phoneNumberHint - ?.takeIf { it.isNotBlank() } - val phoneNumber = PhoneNumberUtils.formatNumberToE164( - subscriptionInfo.number, - subscriptionInfo.countryIso - ) ?: phoneNumberHint ?: subscriptionInfo.number.orEmpty() + val phoneNumber = resolveSimReadableNumber( + formattedE164 = PhoneNumberUtils.formatNumberToE164( + subscriptionInfo.number, + subscriptionInfo.countryIso + ), + phoneNumberHint = phoneNumberHint, + rawSubscriptionNumber = subscriptionInfo.number + ) val iccid = subscriptionInfo.iccId ?: "" Verification( diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolver.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolver.kt new file mode 100644 index 0000000000..d0b01f9f94 --- /dev/null +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolver.kt @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.proto.builder + +/** + * Resolves the SIM-readable phone number sent in Constellation sync requests. + * + * Google Messages may supply a phoneNumberHint when E.164 formatting from the + * subscription record is unavailable (common on dual-SIM or incomplete SIM profiles). + */ +internal fun resolveSimReadableNumber( + formattedE164: String?, + phoneNumberHint: String?, + rawSubscriptionNumber: String? +): String { + if (!formattedE164.isNullOrBlank()) { + return formattedE164 + } + if (!phoneNumberHint.isNullOrBlank()) { + return phoneNumberHint + } + return rawSubscriptionNumber.orEmpty() +} diff --git a/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolverTest.kt b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolverTest.kt new file mode 100644 index 0000000000..fa3f33b30a --- /dev/null +++ b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/proto/builder/SyncRequestPhoneNumberResolverTest.kt @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.proto.builder + +import org.junit.Assert.assertEquals +import org.junit.Test + +class SyncRequestPhoneNumberResolverTest { + + @Test + fun prefersFormattedE164() { + assertEquals( + "+15551234567", + resolveSimReadableNumber( + formattedE164 = "+15551234567", + phoneNumberHint = "+15559876543", + rawSubscriptionNumber = "5551234567" + ) + ) + } + + @Test + fun fallsBackToPhoneNumberHintWhenE164Missing() { + assertEquals( + "+15559876543", + resolveSimReadableNumber( + formattedE164 = null, + phoneNumberHint = "+15559876543", + rawSubscriptionNumber = "5551234567" + ) + ) + } + + @Test + fun fallsBackToRawNumberWhenHintBlank() { + assertEquals( + "5551234567", + resolveSimReadableNumber( + formattedE164 = "", + phoneNumberHint = " ", + rawSubscriptionNumber = "5551234567" + ) + ) + } + + @Test + fun returnsEmptyWhenAllInputsMissing() { + assertEquals( + "", + resolveSimReadableNumber( + formattedE164 = null, + phoneNumberHint = null, + rawSubscriptionNumber = null + ) + ) + } + + @Test + fun skipsBlankE164ForHint() { + assertEquals( + "+491701234567", + resolveSimReadableNumber( + formattedE164 = " ", + phoneNumberHint = "+491701234567", + rawSubscriptionNumber = null + ) + ) + } +} diff --git a/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/Fips186PrfTest.kt b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/Fips186PrfTest.kt new file mode 100644 index 0000000000..fd72882b7b --- /dev/null +++ b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/ts43/Fips186PrfTest.kt @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.verification.ts43 + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test + +class Fips186PrfTest { + + @Test + fun deriveKeys_returnsExpectedKeyLengths() { + val keys = Fips186Prf.deriveKeys( + identityBytes = byteArrayOf(0x01, 0x02, 0x03), + ik = ByteArray(16) { it.toByte() }, + ck = ByteArray(16) { (it + 16).toByte() } + ) + + assertEquals(16, keys["K_encr"]!!.size) + assertEquals(16, keys["K_aut"]!!.size) + assertEquals(64, keys["MSK"]!!.size) + assertEquals(64, keys["EMSK"]!!.size) + } + + @Test + fun deriveKeys_isDeterministic() { + val identity = "310260123456789".toByteArray(Charsets.UTF_8) + val ik = ByteArray(16) { 0x11 } + val ck = ByteArray(16) { 0x22 } + + val first = Fips186Prf.deriveKeys(identity, ik, ck) + val second = Fips186Prf.deriveKeys(identity, ik, ck) + + assertArrayEquals(first["K_encr"], second["K_encr"]) + assertArrayEquals(first["K_aut"], second["K_aut"]) + assertArrayEquals(first["MSK"], second["MSK"]) + assertArrayEquals(first["EMSK"], second["EMSK"]) + } + + @Test + fun deriveKeys_changesWhenIdentityChanges() { + val ik = ByteArray(16) { 0x11 } + val ck = ByteArray(16) { 0x22 } + + val a = Fips186Prf.deriveKeys("imsi-a".toByteArray(), ik, ck) + val b = Fips186Prf.deriveKeys("imsi-b".toByteArray(), ik, ck) + + assertEquals(false, a["MSK"]!!.contentEquals(b["MSK"])) + } +} diff --git a/play-services-droidguard/build.gradle b/play-services-droidguard/build.gradle index 78d9af9a3a..9b9c624f32 100644 --- a/play-services-droidguard/build.gradle +++ b/play-services-droidguard/build.gradle @@ -26,6 +26,11 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + testOptions { + unitTests.returnDefaultValues = true } compileOptions { @@ -46,4 +51,6 @@ dependencies { api project(':play-services-base') implementation "androidx.annotation:annotation:$annotationVersion" + + testImplementation 'junit:junit:4.13.2' } diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt index fc009e8b75..18d018b4fa 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt @@ -135,7 +135,7 @@ class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFacto }) val signed: SignedResponse = future.get() val response = signed.unpack() - val vmKey = response.vmChecksum!!.hex().uppercase(Locale.US) + val vmKey = formatVmCacheKey(response.vmChecksum!!.hex()) if (!isValidCache(vmKey)) { val temp = File(getCacheDir(), "${UUID.randomUUID()}.apk") temp.parentFile!!.mkdirs() diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DroidGuardVmCache.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DroidGuardVmCache.kt new file mode 100644 index 0000000000..9232f82247 --- /dev/null +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DroidGuardVmCache.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard + +import java.util.Locale + +/** Matches stock GMS: context.getDir("dg_cache", MODE_PRIVATE) → app_dg_cache/ */ +const val DG_CACHE_FOLDER_NAME = "dg_cache" + +/** + * Stock GMS uses uppercase hex for VM cache subdirectory names. + * okio ByteString.hex() returns lowercase, which mismatches /proc/self/maps paths. + */ +fun formatVmCacheKey(vmChecksumHex: String): String = vmChecksumHex.uppercase(Locale.US) diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt index b1d0d7708e..337d36fe6c 100644 --- a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt @@ -10,7 +10,6 @@ import android.os.Bundle import android.os.ParcelFileDescriptor import android.os.Parcelable import androidx.annotation.GuardedBy -import org.microg.gms.droidguard.DgVmClassLoader import java.io.File import java.io.IOException import java.security.MessageDigest @@ -49,7 +48,7 @@ open class HandleProxyFactory(private val context: Context) { } fun getTheApkFile(vmKey: String) = File(getCacheDir(vmKey), "the.apk") - protected fun getCacheDir() = context.getDir(CACHE_FOLDER_NAME, Context.MODE_PRIVATE) + protected fun getCacheDir() = context.getDir(DG_CACHE_FOLDER_NAME, Context.MODE_PRIVATE) protected fun getCacheDir(vmKey: String) = File(getCacheDir(), vmKey) protected fun getOptDir(vmKey: String) = File(getCacheDir(vmKey), "opt") protected fun isValidCache(vmKey: String) = getTheApkFile(vmKey).isFile && getOptDir(vmKey).isDirectory @@ -98,7 +97,8 @@ open class HandleProxyFactory(private val context: Context) { companion object { const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" - const val CACHE_FOLDER_NAME = "dg_cache" + @Deprecated("Use DG_CACHE_FOLDER_NAME", ReplaceWith("DG_CACHE_FOLDER_NAME")) + const val CACHE_FOLDER_NAME = DG_CACHE_FOLDER_NAME private val CLASS_MAP = hashMapOf>() val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) } diff --git a/play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java b/play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java new file mode 100644 index 0000000000..77ec820044 --- /dev/null +++ b/play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.droidguard.internal; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class DroidGuardInitReplyTest { + + @Test + public void createFromParcel_returnsNullWhenBothFieldsMissing() { + Parcel parcel = Parcel.obtain(); + parcel.writeParcelable(null, 0); + parcel.writeParcelable(null, 0); + parcel.setDataPosition(0); + + assertNull(DroidGuardInitReply.CREATOR.createFromParcel(parcel)); + parcel.recycle(); + } + + @Test + public void createFromParcel_preservesObjectWhenPfdMissing() { + Bundle extras = new Bundle(); + extras.putString("flow", "tachyon_registration"); + + Parcel parcel = Parcel.obtain(); + parcel.writeParcelable(null, 0); + parcel.writeParcelable(extras, 0); + parcel.setDataPosition(0); + + DroidGuardInitReply reply = DroidGuardInitReply.CREATOR.createFromParcel(parcel); + parcel.recycle(); + + assertNotNull(reply); + assertNull(reply.pfd); + assertSame(extras, reply.object); + } + + @Test + public void roundTrip_preservesParcelableExtras() { + Bundle extras = new Bundle(); + extras.putString("clientVersion", "252432031"); + + DroidGuardInitReply original = new DroidGuardInitReply(null, extras); + Parcel parcel = Parcel.obtain(); + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + DroidGuardInitReply restored = DroidGuardInitReply.CREATOR.createFromParcel(parcel); + parcel.recycle(); + + assertNotNull(restored); + Parcelable restoredExtras = restored.object; + assertNotNull(restoredExtras); + assertSame(extras, restoredExtras); + } +} diff --git a/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt b/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt new file mode 100644 index 0000000000..d5c0d232b6 --- /dev/null +++ b/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Test + +class DgVmClassLoaderTest { + + @Test + fun usesNamedClassInsteadOfAnonymousDexClassLoader() { + assertEquals("DgVmClassLoader", DgVmClassLoader::class.java.simpleName) + assertFalse(DgVmClassLoader::class.java.simpleName.contains("org.microg")) + } +} diff --git a/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DroidGuardVmCacheTest.kt b/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DroidGuardVmCacheTest.kt new file mode 100644 index 0000000000..356337b319 --- /dev/null +++ b/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DroidGuardVmCacheTest.kt @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class DroidGuardVmCacheTest { + + @Test + fun cacheFolderMatchesStockGms() { + assertEquals("dg_cache", DG_CACHE_FOLDER_NAME) + assertNotEquals("cache_dg", DG_CACHE_FOLDER_NAME) + } + + @Test + fun formatVmCacheKey_uppercasesLowercaseHex() { + assertEquals( + "A1B2C3D4E5F6789012345678ABCDEF012345", + formatVmCacheKey("a1b2c3d4e5f6789012345678abcdef012345") + ) + } + + @Test + fun formatVmCacheKey_preservesUppercaseHex() { + val hex = "DEADBEEFCAFEBABE" + assertEquals(hex, formatVmCacheKey(hex)) + } +} From 56749acdaad53cfb6c97d5d528283bfcf7e9f269 Mon Sep 17 00:00:00 2001 From: nwinkelman2 Date: Wed, 2 Sep 2026 10:33:18 -0400 Subject: [PATCH 57/63] test: make DroidGuard parcel semantics runnable on JVM --- .../internal/DroidGuardInitReply.java | 13 +++-- .../internal/DroidGuardInitReplyTest.java | 55 +++++++------------ 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java index fb5f8e5bfc..c015f1b2c3 100644 --- a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java +++ b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardInitReply.java @@ -31,15 +31,20 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(object, flags); } + @Nullable + static DroidGuardInitReply fromNullableFields(@Nullable ParcelFileDescriptor pfd, @Nullable Parcelable object) { + if (pfd == null && object == null) { + return null; + } + return new DroidGuardInitReply(pfd, object); + } + public final static Creator CREATOR = new Creator() { @Override public DroidGuardInitReply createFromParcel(Parcel source) { ParcelFileDescriptor pfd = source.readParcelable(ParcelFileDescriptor.class.getClassLoader()); Parcelable object = source.readParcelable(getClass().getClassLoader()); - if (pfd == null && object == null) { - return null; - } - return new DroidGuardInitReply(pfd, object); + return fromNullableFields(pfd, object); } @Override diff --git a/play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java b/play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java index 77ec820044..08db7e4d53 100644 --- a/play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java +++ b/play-services-droidguard/src/test/java/com/google/android/gms/droidguard/internal/DroidGuardInitReplyTest.java @@ -5,7 +5,6 @@ package com.google.android.gms.droidguard.internal; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -16,52 +15,36 @@ import static org.junit.Assert.assertSame; public class DroidGuardInitReplyTest { + private static final Parcelable TEST_OBJECT = new Parcelable() { + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // The local JVM test exercises nullable-field semantics, not Android's Parcel runtime. + } + }; @Test - public void createFromParcel_returnsNullWhenBothFieldsMissing() { - Parcel parcel = Parcel.obtain(); - parcel.writeParcelable(null, 0); - parcel.writeParcelable(null, 0); - parcel.setDataPosition(0); - - assertNull(DroidGuardInitReply.CREATOR.createFromParcel(parcel)); - parcel.recycle(); + public void fromNullableFields_returnsNullWhenBothFieldsMissing() { + assertNull(DroidGuardInitReply.fromNullableFields(null, null)); } @Test - public void createFromParcel_preservesObjectWhenPfdMissing() { - Bundle extras = new Bundle(); - extras.putString("flow", "tachyon_registration"); - - Parcel parcel = Parcel.obtain(); - parcel.writeParcelable(null, 0); - parcel.writeParcelable(extras, 0); - parcel.setDataPosition(0); - - DroidGuardInitReply reply = DroidGuardInitReply.CREATOR.createFromParcel(parcel); - parcel.recycle(); + public void fromNullableFields_preservesObjectWhenPfdMissing() { + DroidGuardInitReply reply = DroidGuardInitReply.fromNullableFields(null, TEST_OBJECT); assertNotNull(reply); assertNull(reply.pfd); - assertSame(extras, reply.object); + assertSame(TEST_OBJECT, reply.object); } @Test - public void roundTrip_preservesParcelableExtras() { - Bundle extras = new Bundle(); - extras.putString("clientVersion", "252432031"); - - DroidGuardInitReply original = new DroidGuardInitReply(null, extras); - Parcel parcel = Parcel.obtain(); - original.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - - DroidGuardInitReply restored = DroidGuardInitReply.CREATOR.createFromParcel(parcel); - parcel.recycle(); + public void constructorPreservesParcelableObject() { + DroidGuardInitReply reply = new DroidGuardInitReply(null, TEST_OBJECT); - assertNotNull(restored); - Parcelable restoredExtras = restored.object; - assertNotNull(restoredExtras); - assertSame(extras, restoredExtras); + assertSame(TEST_OBJECT, reply.object); } } From 0266edd1694cbeac340b59d9287a50112465e0a4 Mon Sep 17 00:00:00 2001 From: nwinkelman2 Date: Wed, 2 Sep 2026 10:44:19 -0400 Subject: [PATCH 58/63] fix: keep DroidGuard on the platform class loader --- .../microg/gms/droidguard/DgVmClassLoader.kt | 21 ------------------- .../gms/droidguard/HandleProxyFactory.kt | 3 ++- .../gms/droidguard/DgVmClassLoaderTest.kt | 19 ----------------- 3 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt delete mode 100644 play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt deleted file mode 100644 index f0b6461cff..0000000000 --- a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/DgVmClassLoader.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.droidguard - -import dalvik.system.DexClassLoader - -/** - * Named DexClassLoader for DroidGuard VM bytecode. - * - * Anonymous DexClassLoader instances leak "org.microg" in class names visible to - * DroidGuard's /proc/self/maps inspection, causing tachyon_registration failures. - */ -class DgVmClassLoader( - dexPath: String, - optimizedDirectory: String, - librarySearchPath: String?, - parent: ClassLoader -) : DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt index 337d36fe6c..391fdefa37 100644 --- a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt @@ -10,6 +10,7 @@ import android.os.Bundle import android.os.ParcelFileDescriptor import android.os.Parcelable import androidx.annotation.GuardedBy +import dalvik.system.DexClassLoader import java.io.File import java.io.IOException import java.security.MessageDigest @@ -88,7 +89,7 @@ open class HandleProxyFactory(private val context: Context) { getCacheDir(vmKey).deleteRecursively() throw ClassNotFoundException("APK signature verification failed") } - val loader = DgVmClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) + val loader = DexClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) val clazz = loader.loadClass(CLASS_NAME) CLASS_MAP[vmKey] = clazz return clazz diff --git a/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt b/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt deleted file mode 100644 index d5c0d232b6..0000000000 --- a/play-services-droidguard/src/test/kotlin/org/microg/gms/droidguard/DgVmClassLoaderTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.droidguard - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Test - -class DgVmClassLoaderTest { - - @Test - fun usesNamedClassInsteadOfAnonymousDexClassLoader() { - assertEquals("DgVmClassLoader", DgVmClassLoader::class.java.simpleName) - assertFalse(DgVmClassLoader::class.java.simpleName.contains("org.microg")) - } -} From 063ccbea736763234b439f4a5802ccbe18e27649 Mon Sep 17 00:00:00 2001 From: Naorem Nganthoiba Singh Date: Sat, 5 Sep 2026 17:05:01 +0530 Subject: [PATCH 59/63] fix(droidguard): resolve hardcoded hasAccount in NetworkHandleProxyFactory hasAccount in NetworkHandleProxyFactory.createRequest() was hardcoded to false. This reads the value dynamically from AccountManager for 'com.google' accounts, incorporating a SecurityException fallback to false. --- .../gms/droidguard/core/NetworkHandleProxyFactory.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt index 18d018b4fa..1681f41170 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt @@ -5,6 +5,7 @@ package org.microg.gms.droidguard.core +import android.accounts.AccountManager import android.content.Context import com.android.volley.NetworkResponse import com.android.volley.VolleyError @@ -22,6 +23,7 @@ import java.util.* import com.android.volley.Request as VolleyRequest import com.android.volley.Response as VolleyResponse + class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFactory(context) { private val dgDb: DgDatabaseHelper = DgDatabaseHelper(context) private val version = VersionUtil(context) @@ -93,7 +95,12 @@ class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFacto ), versionName = version.versionString, versionCode = BuildConfig.VERSION_CODE, - hasAccount = false, + hasAccount = try { + context.getSystemService(AccountManager::class.java) + ?.getAccountsByType("com.google")?.isNotEmpty() ?: false + } catch (e: SecurityException) { + false + }, isGoogleCn = false, enableInlineVm = true, cached = getCacheDir().list()?.map { it.decodeHex() }.orEmpty(), From 516c1eb5af060c4330c9b6a25318f20c58fddd0c Mon Sep 17 00:00:00 2001 From: KeelTrace <218190424+keeltrace@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:08:15 -0700 Subject: [PATCH 60/63] fix(constellation): align automatic RCS consent request --- .../constellation/core/VerifyPhoneNumber.kt | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt index 9154d33134..4a20eff097 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/VerifyPhoneNumber.kt @@ -30,6 +30,7 @@ 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.RequestTrigger import org.microg.gms.constellation.core.proto.SetConsentRequest import org.microg.gms.constellation.core.proto.SyncRequest import org.microg.gms.constellation.core.proto.Verification @@ -50,6 +51,20 @@ private enum class ReadCallbackMode { TYPED } +internal data class RcsAutoConsentRequestSemantics( + val rcsConsentVersion: ConsentVersion, + val requestConsentVersion: ConsentVersion, + val triggerType: RequestTrigger.Type +) + +internal fun resolveRcsAutoConsentRequestSemantics( + consentType: ConsentVersion +): RcsAutoConsentRequestSemantics = RcsAutoConsentRequestSemantics( + rcsConsentVersion = ConsentVersion.RCS_CONSENT, + requestConsentVersion = consentType, + triggerType = RequestTrigger.Type.CONSENT_API_TRIGGER +) + @Suppress("DEPRECATION") suspend fun handleVerifyPhoneNumberV1( context: Context, @@ -301,14 +316,21 @@ private suspend fun runVerificationFlow( if (!consented) { Log.e(TAG, "Consent has not been set. Auto-setting consent.") val consentType = parseConsentVersion(request.extras) + val semantics = resolveRcsAutoConsentRequestSemantics(consentType) val setRequest = SetConsentRequest( - header_ = RequestHeader(context, sessionId, buildContext, "setConsent"), + header_ = RequestHeader( + context, + sessionId, + buildContext, + "setConsent", + semantics.triggerType + ), asterism_client = asterismClient, rcs_consent = RcsConsent( consent = Consent.CONSENTED, - consent_version = consentType + consent_version = semantics.rcsConsentVersion ), - consent_version = consentType, + consent_version = semantics.requestConsentVersion, api_params = Param.getList(request.extras) ) try { From d8dfdffa88e72269a5a73a657c3bf711af8143cf Mon Sep 17 00:00:00 2001 From: KeelTrace <218190424+keeltrace@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:08:27 -0700 Subject: [PATCH 61/63] test(constellation): cover automatic RCS consent semantics --- .../RcsAutoConsentRequestSemanticsTest.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/RcsAutoConsentRequestSemanticsTest.kt diff --git a/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/RcsAutoConsentRequestSemanticsTest.kt b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/RcsAutoConsentRequestSemanticsTest.kt new file mode 100644 index 0000000000..cf25864acc --- /dev/null +++ b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/RcsAutoConsentRequestSemanticsTest.kt @@ -0,0 +1,33 @@ +package org.microg.gms.constellation.core + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.microg.gms.constellation.core.proto.ConsentVersion +import org.microg.gms.constellation.core.proto.RequestTrigger + +class RcsAutoConsentRequestSemanticsTest { + @Test + fun preservesRequestedConsentVersionButUsesRcsConsentRecordVersion() { + val semantics = resolveRcsAutoConsentRequestSemantics( + ConsentVersion.RCS_DEFAULT_ON_LEGAL_FYI + ) + + assertEquals(ConsentVersion.RCS_CONSENT, semantics.rcsConsentVersion) + assertEquals( + ConsentVersion.RCS_DEFAULT_ON_LEGAL_FYI, + semantics.requestConsentVersion + ) + } + + @Test + fun usesConsentApiTrigger() { + val semantics = resolveRcsAutoConsentRequestSemantics( + ConsentVersion.RCS_DEFAULT_ON_OUT_OF_BOX + ) + + assertEquals( + RequestTrigger.Type.CONSENT_API_TRIGGER, + semantics.triggerType + ) + } +} From edcd8f851c5eb98668f9b9ec00d2b0eaad3bc15a Mon Sep 17 00:00:00 2001 From: HumbleDrummer <280771122+HumbleDrummer@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:50:11 +0200 Subject: [PATCH 62/63] fix(constellation): assemble multipart MT SMS Assemble the ordered PDU parts from a single SMS_RECEIVED broadcast before matching and buffering the logical message. Reject missing parts and keep the first available sender. Co-authored-by: Codex --- .../core/verification/MtSmsVerifier.kt | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt index d4beb0179a..1f8d4d895f 100644 --- a/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt +++ b/play-services-constellation/core/src/main/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifier.kt @@ -78,6 +78,17 @@ internal data class ReceivedSms( val sender: String ) +internal fun assembleReceivedSms( + bodyParts: List, + sender: String? +): ReceivedSms? { + if (bodyParts.isEmpty() || bodyParts.any { it == null }) return null + return ReceivedSms( + body = bodyParts.joinToString(separator = "") { it.orEmpty() }, + sender = sender.orEmpty() + ) +} + private data class PendingMatch( val expectedBody: String, val continuation: CancellableContinuation @@ -138,28 +149,24 @@ internal class MtSmsInbox( } private fun onMessagesReceived(messages: Array) { - val receivedMessages = messages.mapNotNull { message -> - val body = message.messageBody ?: return@mapNotNull null - ReceivedSms( - body = body, - sender = message.originatingAddress ?: "" - ) - } - if (receivedMessages.isEmpty()) return + // Android provides the ordered parts of one logical SMS in each broadcast; + // valid multipart messages share a sender, so keep the first available address. + val receivedMessage = assembleReceivedSms( + bodyParts = messages.map { it.messageBody }, + sender = messages.firstNotNullOfOrNull { it.originatingAddress } + ) ?: 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) - } + bufferedMessages += receivedMessage + 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) } } } From 6af8c095ad8bc4852e0b100e489616d74200682f Mon Sep 17 00:00:00 2001 From: HumbleDrummer <280771122+HumbleDrummer@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:53:20 +0200 Subject: [PATCH 63/63] test(constellation): cover MT SMS assembly contract Cover multipart ordering, invalid or missing parts, and single-part/null-sender behavior. Co-authored-by: Codex --- .../core/verification/MtSmsVerifierTest.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifierTest.kt diff --git a/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifierTest.kt b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifierTest.kt new file mode 100644 index 0000000000..8b3d66fcba --- /dev/null +++ b/play-services-constellation/core/src/test/kotlin/org/microg/gms/constellation/core/verification/MtSmsVerifierTest.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation.core.verification + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class MtSmsVerifierTest { + + @Test + fun assemblesMultipartSmsWhenChallengeSpansParts() { + assertEquals( + ReceivedSms( + body = "Your verification code is 123456. Do not share it.", + sender = "+15551234567" + ), + assembleReceivedSms( + bodyParts = listOf( + "Your verification code is 123", + "456. Do not share it." + ), + sender = "+15551234567" + ) + ) + } + + @Test + fun preservesSinglePartSmsAndDefaultsMissingSender() { + assertEquals( + ReceivedSms( + body = "Your verification code is 123456.", + sender = "" + ), + assembleReceivedSms( + bodyParts = listOf("Your verification code is 123456."), + sender = null + ) + ) + } + + @Test + fun ignoresEmptySmsBroadcast() { + assertNull( + assembleReceivedSms( + bodyParts = emptyList(), + sender = null + ) + ) + } + + @Test + fun ignoresMultipartSmsWithMissingBodyPart() { + assertNull( + assembleReceivedSms( + bodyParts = listOf("verification code: 123", null, "456"), + sender = "+15551234567" + ) + ) + } +}