Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 16770bc8 authored by Fynn Godau's avatar Fynn Godau Committed by Jonathan Klee
Browse files

Refactor licensing code to use coroutines

parent 34445371
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -141,10 +141,10 @@
        </activity>

        <receiver
            android:name="com.android.vending.licensing.LicenseServiceNotificationRunnable$IgnoreReceiver"
            android:name="com.android.vending.licensing.IgnoreReceiver"
            android:exported="false" />
        <receiver
            android:name="com.android.vending.licensing.LicenseServiceNotificationRunnable$SignInReceiver"
            android:name="com.android.vending.licensing.SignInReceiver"
            android:exported="false" />

        <activity
+33 −16
Original line number Diff line number Diff line
package com.android.vending

import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AccountManagerFuture
import android.os.Bundle
import android.util.Log
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.zip.GZIPOutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

object Util {
private const val TAG = "FakeStoreUtil"

/**
 * From [StackOverflow](https://stackoverflow.com/a/46688434/), CC BY-SA 4.0 by Sergey Frolov, adapted.
 */
    fun encodeGzip(input: ByteArray): ByteArray {
fun ByteArray.encodeGzip(): ByteArray {
    try {
        ByteArrayOutputStream().use { byteOutput ->
            GZIPOutputStream(byteOutput).use { gzipOutput ->
                    gzipOutput.write(input)
                gzipOutput.write(this)
                gzipOutput.finish()
                return byteOutput.toByteArray()
            }
@@ -25,4 +31,15 @@ object Util {
        return ByteArray(0)
    }
}

suspend fun AccountManager.getAuthToken(account: Account, authTokenType: String, notifyAuthFailure: Boolean) =
    suspendCoroutine { continuation ->
        getAuthToken(account, authTokenType, notifyAuthFailure, { future: AccountManagerFuture<Bundle> ->
            try {
                val result = future.result
                continuation.resume(result)
            } catch (e: Exception) {
                continuation.resumeWithException(e)
            }
        }, null)
    }
 No newline at end of file
+127 −188
Original line number Diff line number Diff line
@@ -2,148 +2,16 @@ package com.android.vending.licensing

import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AccountManagerFuture
import android.accounts.AuthenticatorException
import android.accounts.OperationCanceledException
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.RemoteException
import android.util.Log
import com.android.vending.V1Container
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.vending.getAuthToken
import com.android.volley.VolleyError
import org.microg.vending.billing.core.HttpClient
import java.io.IOException

/**
 * Performs license check including caller UID verification, using a given account, for which
 * an auth token is fetched.
 *
 * @param D Request parameter data value type
 * @param R Result type
 */
abstract class LicenseChecker<D, R> {
    abstract fun createRequest(
        packageName: String, auth: String, versionCode: Int, data: D,
        then: (Int, R) -> Unit, errorListener: Response.ErrorListener?
    ): LicenseRequest<*>

    @Throws(RemoteException::class)
    fun checkLicense(
        account: Account?, accountManager: AccountManager, androidId: String?,
        packageName: String, callingUid: Int, packageManager: PackageManager,
        queue: RequestQueue, queryData: D,
        onResult: (Int, R?) -> Unit
    ) {
        try {
            val packageInfo = packageManager.getPackageInfo(packageName, 0)
            val versionCode = packageInfo.versionCode

            // Verify caller identity
            if (packageInfo.applicationInfo.uid != callingUid) {
                Log.e(
                    TAG,
                    "an app illegally tried to request licenses for another app (caller: $callingUid)"
                )
                onResult.safeSendResult(ERROR_NON_MATCHING_UID, null)
            } else {
                val onRequestFinished: (Int, R) -> Unit = { integer: Int, r: R ->
                    onResult.safeSendResult(integer, r)
                    }

                val onRequestError = Response.ErrorListener { error: VolleyError ->
                    Log.e(TAG, "license request failed with $error")
                    onResult.safeSendResult(ERROR_CONTACTING_SERVER, null)
                }

                accountManager.getAuthToken(
                    account, AUTH_TOKEN_SCOPE, false,
                    { future: AccountManagerFuture<Bundle> ->
                        try {
                            val auth = future.result.getString(AccountManager.KEY_AUTHTOKEN)
                            if (auth == null) {
                                onResult.safeSendResult(ERROR_CONTACTING_SERVER, null)
                            } else {
                                val request = createRequest(
                                    packageName, auth,
                                    versionCode, queryData, onRequestFinished, onRequestError
                                )

                                if (androidId != null) {
                                    request.ANDROID_ID = androidId.toLong(16)
                                }

                                request.setShouldCache(false)
                                queue.add(request)
                            }
                        } catch (e: AuthenticatorException) {
                            onResult.safeSendResult(ERROR_CONTACTING_SERVER, null)
                        } catch (e: IOException) {
                            onResult.safeSendResult(ERROR_CONTACTING_SERVER, null)
                        } catch (e: OperationCanceledException) {
                            onResult.safeSendResult(ERROR_CONTACTING_SERVER, null)
                        }
                    }, null
                )
            }
        } catch (e: PackageManager.NameNotFoundException) {
            Log.e(
                TAG,
                "an app tried to request licenses for package $packageName, which does not exist"
            )
            onResult.safeSendResult(ERROR_INVALID_PACKAGE_NAME, null)
        }
    }

    // Implementations
    class V1 : LicenseChecker<Long, Pair<String?, String?>?>() {
        override fun createRequest(
            packageName: String,
            auth: String,
            versionCode: Int,
            nonce: Long,
            then: (Int, Pair<String?, String?>?) -> Unit,
            errorListener: Response.ErrorListener?
        ): LicenseRequest<V1Container> {
            return LicenseRequest.V1(
                packageName, auth, versionCode, nonce, { response: V1Container? ->
                    if (response != null) {
                        Log.v(TAG,
                            "licenseV1 result was ${response.result} with signed data ${response.signedData}"
                        )

                        if (response.result != null) {
                            then(
                                response.result,
                                (response.signedData to response.signature)
                            )
                        } else {
                            then(LICENSED, response.signedData to response.signature)
                        }
                    }
                }, errorListener
            )
        }
    }

    class V2 : LicenseChecker<Unit, String?>() {
        override fun createRequest(
            packageName: String, auth: String, versionCode: Int, data: Unit,
            then: (Int, String?) -> Unit, errorListener: Response.ErrorListener?
        ): LicenseRequest<String> {
            return LicenseRequest.V2(
                packageName, auth, versionCode, { response: String? ->
                    if (response != null) {
                        then(LICENSED, response)
                    } else {
                        then(NOT_LICENSED, null)
                    }
                }, errorListener
            )
        }
    }

    companion object {
private const val TAG = "FakeLicenseChecker"

/* Possible response codes for checkLicense v1, from
@@ -197,16 +65,87 @@ abstract class LicenseChecker<D, R> {
const val ERROR_NON_MATCHING_UID: Int = 0x103

const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/googleplay"

/**
 * Performs license check including caller UID verification, using a given account, for which
 * an auth token is fetched.
 */
@Throws(RemoteException::class)
suspend fun HttpClient.checkLicense(
    account: Account, accountManager: AccountManager, androidId: String?,
    packageName: String, callingUid: Int, packageManager: PackageManager, queryData: RequestParameters
) : LicenseResponse {
    val packageInfo = try {
        packageManager.getPackageInfo(packageName, 0)
    } catch (e: PackageManager.NameNotFoundException) {
        Log.e(TAG,
            "an app tried to request licenses for package $packageName, which does not exist"
        )
        return ErrorResponse(ERROR_INVALID_PACKAGE_NAME)
    }
    val versionCode = packageInfo.versionCode

    private fun <A, B> ((A, B?) -> Unit).safeSendResult(
        a: A, b: B
    ) {
        try {
            this(a, b)
        } catch (e: Exception) {
            Log.e(TAG, "While sending result $a, $b, remote encountered an exception.")
            e.printStackTrace()
    // Verify caller identity
    if (packageInfo.applicationInfo.uid != callingUid) {
        Log.e(
            TAG,
            "an app illegally tried to request licenses for another app (caller: $callingUid)"
        )
        return ErrorResponse(ERROR_NON_MATCHING_UID)
    }

    val auth = try {
        accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false)
            .getString(AccountManager.KEY_AUTHTOKEN)
    } catch (e: AuthenticatorException) {
        Log.e(TAG, "Could not fetch auth token for account $account")
        return ErrorResponse(ERROR_CONTACTING_SERVER)
    }

    if (auth == null) {
        return ErrorResponse(ERROR_CONTACTING_SERVER)
    }

    val decodedAndroidId = androidId?.toLong(16) ?: 1

    return try {
        when (queryData) {
            is V1Request -> makeLicenseV1Request(
                packageName, auth, versionCode, queryData.nonce, decodedAndroidId
            )
            is V2Request -> makeLicenseV2Request(
                packageName, auth, versionCode, decodedAndroidId
            )
        } ?: ErrorResponse(NOT_LICENSED)
    } catch (e: VolleyError) {
        Log.e(TAG, "License request failed with $e")
        ErrorResponse(ERROR_CONTACTING_SERVER)
    } catch (e: IOException) {
        Log.e(TAG, "Encountered a network error during operation ($e)")
        ErrorResponse(ERROR_CONTACTING_SERVER)
    } catch (e: OperationCanceledException) {
        ErrorResponse(ERROR_CONTACTING_SERVER)
    }
}

sealed class RequestParameters
data class V1Request(
    val nonce: Long
) : RequestParameters()
object V2Request : RequestParameters()

sealed class LicenseResponse(
    val result: Int
)
class V1Response(
    result: Int,
    val signedData: String,
    val signature: String
) : LicenseResponse(result)
class V2Response(
    result: Int,
    val jwt: String?
): LicenseResponse(result)
class ErrorResponse(
    result: Int
): LicenseResponse(result)
+147 −204
Original line number Diff line number Diff line
@@ -21,32 +21,47 @@ import com.android.vending.TimestampStringWrapper
import com.android.vending.TimestampWrapper
import com.android.vending.UnknownByte12
import com.android.vending.UserAgent
import com.android.vending.Util
import com.android.vending.Uuid
import com.android.vending.V1Container
import com.android.volley.NetworkResponse
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.vending.encodeGzip
import com.google.android.gms.common.BuildConfig
import okio.ByteString
import org.microg.gms.profile.Build
import java.io.IOException
import org.microg.vending.billing.core.HttpClient
import java.net.URLEncoder
import java.util.UUID

abstract class LicenseRequest<T> protected constructor(
    url: String,
    private val auth: String?,
    private val successListener: Response.Listener<T>,
    errorListener: Response.ErrorListener?
) : Request<T>(
    Method.GET, url, errorListener
) {
    var ANDROID_ID: Long = 1
private const val TAG = "FakeLicenseRequest"

private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"

suspend fun HttpClient.makeLicenseV1Request(
    packageName: String, auth: String, versionCode: Int, nonce: Long, androidId: Long
): V1Response? = get(
        url = "https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=$packageName&vc=$versionCode&nnc=$nonce",
        headers = getHeaders(auth, androidId),
        adapter = LicenseResult.ADAPTER
    ).information?.v1?.let {
        if (it.result != null && it.signedData != null && it.signature != null) {
            V1Response(it.result, it.signedData, it.signature)
        } else null
    }

    override fun getHeaders(): Map<String, String> {
suspend fun HttpClient.makeLicenseV2Request(
    packageName: String,
    auth: String,
    versionCode: Int,
    androidId: Long
): V2Response? = get(
    url = "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=$packageName&vc=$versionCode",
    headers = getHeaders(auth, androidId),
    adapter = LicenseResult.ADAPTER
).information?.v2?.license?.jwt?.let {
    // Field present ←→ user has license
    V2Response(LICENSED, it)
}

private fun getHeaders(auth: String, androidId: Long): Map<String, String> {
    var millis = System.currentTimeMillis()
    val timestamp = TimestampContainer.Builder()
        .container2(
@@ -59,7 +74,7 @@ abstract class LicenseRequest<T> protected constructor(
    timestamp
        .container1Wrapper(
            TimestampContainer1Wrapper.Builder()
                    .androidId(ANDROID_ID.toString())
                .androidId(androidId.toString())
                .container(
                    TimestampContainer1.Builder()
                        .timestamp(millis.toString() + "000")
@@ -69,7 +84,7 @@ abstract class LicenseRequest<T> protected constructor(
                .build()
        )
    val encodedTimestamps = String(
            Base64.encode(Util.encodeGzip(timestamp.build().encode()), BASE64_FLAGS)
        Base64.encode(timestamp.build().encode().encodeGzip(), BASE64_FLAGS)
    )

    val locality = Locality.Builder()
@@ -127,7 +142,7 @@ abstract class LicenseRequest<T> protected constructor(
                .deviceModelName(Build.MODEL)
                .finskyVersion(FINSKY_VERSION)
                .deviceProductName(Build.MODEL)
                    .androidId(ANDROID_ID) // must not be 0
                .androidId(androidId) // must not be 0
                .buildFingerprint(Build.FINGERPRINT)
                .build()
        )
@@ -138,7 +153,7 @@ abstract class LicenseRequest<T> protected constructor(
                .build()
        )
        .build().encode()
        val xPsRh = String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS))
    val xPsRh = String(Base64.encode(header.encodeGzip(), BASE64_FLAGS))

    Log.v(TAG, "X-PS-RH: $xPsRh")

@@ -159,85 +174,13 @@ abstract class LicenseRequest<T> protected constructor(
    )
}

    override fun deliverResponse(response: T) {
        successListener.onResponse(response)
    }

    class V1(
        packageName: String,
        auth: String?,
        versionCode: Int,
        nonce: Long,
        successListener: (V1Container) -> Unit,
        errorListener: Response.ErrorListener?
    ) : LicenseRequest<V1Container>(
        "https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=$packageName&vc=$versionCode&nnc=$nonce",
        auth, successListener, errorListener
    ) {
        override fun parseNetworkResponse(response: NetworkResponse): Response<V1Container?>? {
            if (response.data != null) {
                try {
                    val result = LicenseResult.ADAPTER.decode(response.data)
                    return Response.success(result.information!!.v1, null)
                } catch (e: IOException) {
                    return Response.error(VolleyError(e))
                } catch (e: NullPointerException) {
                    // A field does not exist → user has no license
                    return Response.success(null, null)
                }
            } else {
                return Response.error(VolleyError("No response was returned"))
            }
        }
    }

    class V2(
        packageName: String,
        auth: String?,
        versionCode: Int,
        successListener: Response.Listener<String>,
        errorListener: Response.ErrorListener?
    ) : LicenseRequest<String>(
        "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=$packageName&vc=$versionCode",
        auth, successListener, errorListener
    ) {
        override fun parseNetworkResponse(response: NetworkResponse): Response<String> {
            if (response.data != null) {
                try {
                    val result = LicenseResult.ADAPTER.decode(response.data)

                    val jwt = result.information?.v2?.license?.jwt
                    return if (jwt != null) {
                        Response.success(jwt, null)
                    } else {
                        // A field does not exist → user has no license
                        Response.success(null, null)
                    }

                } catch (e: IOException) {
                    return Response.error(VolleyError(e))
                }
            } else {
                return Response.error(VolleyError("No response was returned"))
            }
        }
    }

    companion object {
        private const val TAG = "FakeLicenseRequest"

        private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
        private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"

        private fun encodeString(s: String?): String {
            return URLEncoder.encode(s).replace("+", "%20")
        }

private fun makeTimestamp(millis: Long): Timestamp {
    return Timestamp.Builder()
        .seconds((millis / 1000))
        .nanos(((millis % 1000) * 1000000).toInt())
        .build()
}
    }

private fun encodeString(s: String?): String {
    return URLEncoder.encode(s).replace("+", "%20")
}
+7 −6
Original line number Diff line number Diff line
@@ -138,21 +138,22 @@ class IgnoreReceiver : BroadcastReceiver() {
        Log.d(TAG, "Adding package $newIgnorePackage to ignore list")

        ignoreList.add(newIgnorePackage)
        preferences.edit().putStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, ignoreList)
        preferences.edit()
            .putStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, ignoreList)
            .apply()
    }
}

class SignInReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Dismiss all notifications

        // Dismiss all notifications
        NotificationManagerCompat.from(context).cancelAll()

        Log.d(TAG, "Starting sign in activity")
        val authIntent = Intent(GMS_AUTH_INTENT_ACTION)
        authIntent.setPackage(GMS_PACKAGE_NAME)
        authIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        context.startActivity(authIntent)
        Intent(GMS_AUTH_INTENT_ACTION).apply {
            setPackage(GMS_PACKAGE_NAME)
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }.let { context.startActivity(it) }
    }
}
 No newline at end of file
Loading