diff --git a/play-services-api/src/main/aidl/com/google/android/gms/potokens/PoToken.aidl b/play-services-api/src/main/aidl/com/google/android/gms/potokens/PoToken.aidl new file mode 100644 index 0000000000..8cf0bb7ed2 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/potokens/PoToken.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens; + +parcelable PoToken; \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/IPoTokensService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/IPoTokensService.aidl new file mode 100644 index 0000000000..938c848a1f --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/IPoTokensService.aidl @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens.internal; + +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.potokens.internal.ITokenCallbacks; + +interface IPoTokensService { + void responseStatus(IStatusCallback call, int code) = 1; + void responseStatusToken(ITokenCallbacks call, int i, in byte[] bArr) = 2; +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/ITokenCallbacks.aidl b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/ITokenCallbacks.aidl new file mode 100644 index 0000000000..55c71dfe8f --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/ITokenCallbacks.aidl @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens.internal; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.potokens.PoToken; + +interface ITokenCallbacks { + void responseToken(in Status status, in PoToken token) = 1; +} \ No newline at end of file diff --git a/play-services-api/src/main/java/com/google/android/gms/potokens/PoToken.java b/play-services-api/src/main/java/com/google/android/gms/potokens/PoToken.java new file mode 100644 index 0000000000..9f5a3f4c2c --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/potokens/PoToken.java @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class PoToken extends AutoSafeParcelable { + + @Field(1) + public byte[] data; + + public PoToken(byte[] data) { + this.data = data; + } + + public static Creator CREATOR = findCreator(PoToken.class); +} diff --git a/play-services-core-proto/src/main/proto/potoken.proto b/play-services-core-proto/src/main/proto/potoken.proto new file mode 100644 index 0000000000..98475912c2 --- /dev/null +++ b/play-services-core-proto/src/main/proto/potoken.proto @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +option java_package = "com.google.android.gms.potokens"; + +option java_multiple_files = true; + +message CipherKey { + optional int32 key = 1; + optional bytes value = 3; +} + +message KeyData { + optional string typeUrl = 1; + optional CipherKey value = 2; + optional int32 keyMaterialType = 3; +} + +message Key { + optional KeyData data = 1; + optional int32 status = 2; + optional int32 keyId = 3; + optional int32 outputPrefixType = 4; +} + +message KeySet { + optional int32 keyId = 1; + repeated Key keyList = 2; +} + +message PoTokenInfo { + optional int32 key = 6; + optional int32 time = 1; + optional bytes inputData = 2; + optional string pkgName = 3; + optional bytes pkgSignSha256 = 4; + optional bytes tokenData = 5; +} + +message GetPoIntegrityTokenRequest { + optional int32 mode = 1; + optional bytes dgResult = 2; + optional bytes dgRandKey = 3; +} + +message GetPoIntegrityTokenResponse { + optional bytes desc = 1; + optional int32 code = 2; + optional bytes backup = 3; +// optional bytes d = 4; +// optional bytes e = 5; +} + +message PoTokenResult { + optional bytes encryptData = 1; + optional bytes tokenData = 2; +} + +message PoTokenResultWrap { + optional PoTokenResult data = 1; +} diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index c07bf4020d..9512401b59 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -712,6 +712,14 @@ + + + + + + + + + ): String? { + try { + val resultTask = DroidGuard.getClient(context).getResults(PoTokenConstants.KEY_TOKEN, map, request) + return Tasks.await(resultTask, 15, TimeUnit.SECONDS) + } catch (e: Throwable) { + Log.w(TAG, "PoTokenHelper getDroidGuardResult exception: $e") + } + return null + } + + private fun getPoIntegrityToken(context: Context, keySet: KeySet): GetPoIntegrityTokenResponse? { + try { + val droidGuardResultsRequest = DroidGuardResultsRequest().apply { + bundle.putByteArray(PoTokenConstants.KEY_FAST, keySet.encode()) + bundle.putStringArrayList(PoTokenConstants.KEY_FALLBACK, arrayListOf(PoTokenConstants.KEY_FAST)) + } + val randKeyBuf = ByteArray(0x20).also { SecureRandom().nextBytes(it) } + val randKey = Base64.encodeToString(randKeyBuf, Base64.NO_WRAP) + val map: MutableMap = HashMap() + map["b"] = randKey + val dgResult = + getDroidGuardResult(context, droidGuardResultsRequest, map)?.encodeToByteArray()?.toByteString() + val tokenRequest = + GetPoIntegrityTokenRequest(dgResult = dgResult, dgRandKey = randKeyBuf.toByteString(), mode = 1) + return postPoTokenForGms(tokenRequest) + } catch (e: Throwable) { + Log.w(TAG, "PoTokenHelper getPoIntegrityToken exception: $e") + } + return null + } + + private fun postPoTokenForGms(request: GetPoIntegrityTokenRequest): GetPoIntegrityTokenResponse? { + val future = RequestFuture.newFuture() + volleyQueue.add(object : Request( + Method.POST, PoTokenConstants.TOKEN_URL, future + ) { + + override fun deliverResponse(response: GetPoIntegrityTokenResponse?) { + Log.d(TAG, "PoTokenHelper postPoTokenForGms response: $response") + future.onResponse(response) + } + + override fun getHeaders(): Map { + return mapOf("User-Agent" to "GmsCore/${Constants.GMS_VERSION_CODE} (${Build.DEVICE} ${Build.ID}); gzip") + } + + override fun getBody(): ByteArray { + return request.encode() + } + + override fun getBodyContentType(): String = "application/x-protobuf" + + override fun parseNetworkResponse(response: NetworkResponse): Response { + return try { + Response.success(GetPoIntegrityTokenResponse.ADAPTER.decode(response.data), null) + } catch (e: Exception) { + Response.error(VolleyError(e)) + } + } + }) + return future.get() + } + + suspend fun callPoToken(context: Context, packageName: String, inputData: ByteArray): ByteArray { + var tokenDesc = PoTokenPreferences.get(context).getString(PoTokenConstants.KEY_DESC, "") + var tokenBackup = PoTokenPreferences.get(context).getString(PoTokenConstants.KEY_BACKUP, "") + var keySetStr = PoTokenPreferences.get(context).getString(PoTokenConstants.KEY_SET_STR, "") + + val keySet = + if (TextUtils.isEmpty(tokenDesc) || TextUtils.isEmpty(tokenBackup) || TextUtils.isEmpty(keySetStr)) { + buildKeySet().also { + Log.d(TAG, "PoTokenHelper postPoTokenForGms start") + val response = withContext(Dispatchers.IO) { getPoIntegrityToken(context, it) } + Log.d(TAG, "PoTokenHelper postPoTokenForGms end") + tokenDesc = Base64.encodeToString(response?.desc?.toByteArray(), Base64.DEFAULT) + tokenBackup = Base64.encodeToString(response?.backup?.toByteArray(), Base64.DEFAULT) + keySetStr = Base64.encodeToString(it.encode(), Base64.DEFAULT) + PoTokenPreferences.get(context).save(PoTokenConstants.KEY_DESC, tokenDesc) + PoTokenPreferences.get(context).save(PoTokenConstants.KEY_BACKUP, tokenBackup) + PoTokenPreferences.get(context).save(PoTokenConstants.KEY_SET_STR, keySetStr) + } + } else { + val result = Base64.decode(keySetStr, Base64.DEFAULT) + KeySet.ADAPTER.decode(result) + } + + val poTokenInfoData: ByteArray = PoTokenInfo( + inputData = inputData.toByteString(), + pkgName = packageName, + pkgSignSha256 = context.packageManager.getFirstSignatureDigest(packageName, "SHA-256")?.toByteString(), + tokenData = Base64.decode(tokenDesc, Base64.DEFAULT).toByteString() + ).encode() + + val keyObj: Key = keySet.keyList[0] + val keyId: Int? = keyObj.keyId + val key: ByteArray? = keyObj.data_?.value_?.value_?.toByteArray() + val iv = ByteArray(12).also { SecureRandom().nextBytes(it) } + + var data = aesEncrypt(key, iv, poTokenInfoData) + keyId?.let { data = concatCipherIdentifier(it, data) } + + val poTokenResult = PoTokenResult( + encryptData = data.toByteString(), + tokenData = Base64.decode(tokenBackup, Base64.DEFAULT).toByteString() + ) + + return PoTokenResultWrap(poTokenResult).encode() + } + + companion object { + @Volatile + private var instance: PoTokenHelper? = null + fun get(context: Context): PoTokenHelper { + return instance ?: synchronized(this) { + instance ?: PoTokenHelper(context).also { instance = it } + } + } + } + +} diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt new file mode 100644 index 0000000000..3eca2cd313 --- /dev/null +++ b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens.utils + +import android.content.Context +import android.content.SharedPreferences +import com.android.volley.toolbox.Volley +import org.microg.gms.droidguard.core.HandleProxyFactory + +class PoTokenPreferences(context: Context) { + + private val sp: SharedPreferences = + context.getSharedPreferences("com.google.android.gms.potokens", Context.MODE_PRIVATE) + + fun save(key: String, value: Any?) { + when (value) { + is String -> { + sp.edit().putString(key, value).apply() + } + + is Int -> { + sp.edit().putInt(key, value).apply() + } + } + } + + fun getString(key: String, defValue: String?): String? { + return sp.getString(key, defValue) + } + + fun getInt(key: String, defValue: Int): Int { + return sp.getInt(key, defValue) + } + + companion object { + @Volatile + private var instance: PoTokenPreferences? = null + fun get(context: Context): PoTokenPreferences { + return instance ?: synchronized(this) { + instance ?: PoTokenPreferences(context).also { instance = it } + } + } + } +} \ No newline at end of file