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

Commit 80a724a7 authored by Fynn Godau's avatar Fynn Godau Committed by Jonathan Klee
Browse files

Share more code between v1 and v2 licensing

parent 16770bc8
Loading
Loading
Loading
Loading
+42 −29
Original line number Diff line number Diff line
@@ -4,9 +4,10 @@ import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AuthenticatorException
import android.accounts.OperationCanceledException
import android.content.pm.PackageManager
import android.content.pm.PackageInfo
import android.os.RemoteException
import android.util.Log
import com.android.vending.LicenseResult
import com.android.vending.getAuthToken
import com.android.volley.VolleyError
import org.microg.vending.billing.core.HttpClient
@@ -72,27 +73,13 @@ const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/goo
 */
@Throws(RemoteException::class)
suspend fun HttpClient.checkLicense(
    account: Account, accountManager: AccountManager, androidId: String?,
    packageName: String, callingUid: Int, packageManager: PackageManager, queryData: RequestParameters
    account: Account,
    accountManager: AccountManager,
    androidId: String?,
    packageInfo: PackageInfo,
    packageName: String,
    queryData: LicenseRequestParameters
) : 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

    // 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)
@@ -110,11 +97,11 @@ suspend fun HttpClient.checkLicense(

    return try {
        when (queryData) {
            is V1Request -> makeLicenseV1Request(
                packageName, auth, versionCode, queryData.nonce, decodedAndroidId
            is V1Parameters -> makeLicenseV1Request(
                packageName, auth, packageInfo.versionCode, queryData.nonce, decodedAndroidId
            )
            is V2Request -> makeLicenseV2Request(
                packageName, auth, versionCode, decodedAndroidId
            is V2Parameters -> makeLicenseV2Request(
                packageName, auth, packageInfo.versionCode, decodedAndroidId
            )
        } ?: ErrorResponse(NOT_LICENSED)
    } catch (e: VolleyError) {
@@ -128,11 +115,37 @@ suspend fun HttpClient.checkLicense(
    }
}

sealed class RequestParameters
data class V1Request(
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 = getLicenseRequestHeaders(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
}

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 = getLicenseRequestHeaders(auth, androidId),
    adapter = LicenseResult.ADAPTER
).information?.v2?.license?.jwt?.let {
    // Field present ←→ user has license
    V2Response(LICENSED, it)
}

sealed class LicenseRequestParameters
data class V1Parameters(
    val nonce: Long
) : RequestParameters()
object V2Request : RequestParameters()
) : LicenseRequestParameters()
object V2Parameters : LicenseRequestParameters()

sealed class LicenseResponse(
    val result: Int
+1 −29
Original line number Diff line number Diff line
@@ -8,7 +8,6 @@ import com.android.vending.EncodedTriple
import com.android.vending.EncodedTripleWrapper
import com.android.vending.IntWrapper
import com.android.vending.LicenseRequestHeader
import com.android.vending.LicenseResult
import com.android.vending.Locality
import com.android.vending.LocalityWrapper
import com.android.vending.StringWrapper
@@ -26,7 +25,6 @@ import com.android.vending.encodeGzip
import com.google.android.gms.common.BuildConfig
import okio.ByteString
import org.microg.gms.profile.Build
import org.microg.vending.billing.core.HttpClient
import java.net.URLEncoder
import java.util.UUID

@@ -35,33 +33,7 @@ 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
    }

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> {
internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map<String, String> {
    var millis = System.currentTimeMillis()
    val timestamp = TimestampContainer.Builder()
        .container2(
+75 −64
Original line number Diff line number Diff line
@@ -37,49 +37,21 @@ class LicensingService : Service() {
            listener: ILicenseResultListener
        ): Unit = runBlocking {
            Log.v(TAG, "checkLicense($nonce, $packageName)")
            val callingUid = getCallingUid()

            if (!isLicensingEnabled(this@LicensingService)) {
                Log.d(TAG, "not checking license, as it is disabled by user")
                return@runBlocking
            }

            val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)
            val packageManager = packageManager

            lateinit var lastResponse: LicenseResponse
            if (accounts.isEmpty()) {
                handleNoAccounts(packageName, packageManager)
                return@runBlocking
            } else for (account: Account in accounts) {

                lastResponse = httpClient.checkLicense(
                    account,
                    accountManager,
                    androidId,
                    packageName,
                    callingUid,
                    packageManager,
                    V1Request(nonce)
                )

                if (lastResponse.result == LICENSED) {
                    // Do not consider further accounts
                    break
                }
            }
            val response = checkLicenseCommon(packageName, V1Parameters(nonce))

            /* If a license is found, it is now stored in `lastResponse`. Otherwise, it now contains
             * an error. In either case, we should send it to the application.
             */
            try {
                when (lastResponse) {
                    is V1Response -> listener.verifyLicense(lastResponse.result, lastResponse.signedData, lastResponse.signature)
                    is ErrorResponse -> listener.verifyLicense(lastResponse.result, null, null)
                when (response) {
                    is V1Response -> listener.verifyLicense(response.result, response.signedData, response.signature)
                    is ErrorResponse -> listener.verifyLicense(response.result, null, null)
                    is V2Response -> Unit // should never happen
                    null -> Unit // no license check was performed at all
                }
            } catch (e: Exception) {
                Log.w(TAG, "Remote threw an exception while returning license result ${lastResponse}")
                Log.w(TAG, "Remote threw an exception while returning license result ${response}")
            }
        }

@@ -90,55 +62,94 @@ class LicensingService : Service() {
            extraParams: Bundle
        ): Unit = runBlocking {
            Log.v(TAG, "checkLicenseV2($packageName, $extraParams)")

            val response = checkLicenseCommon(packageName, V2Parameters)

            /*
             * Suppress failures on V2. V2 is commonly used by free apps whose checker
             * will not throw users out of the app if it never receives a response.
             *
             * This means that users who are signed in to a Google account will not
             * get a worse experience in these apps than users that are not signed in.
             *
             * Normally, we would otherwise always send the response.
             */
            if (response?.result == LICENSED && response is V2Response) {
                val bundle = Bundle()
                bundle.putString(KEY_V2_RESULT_JWT, response.jwt)

                try {
                    listener.verifyLicense(response.result, bundle)
                } catch (e: Exception) {
                    Log.w(TAG, "Remote threw an exception while returning license result ${response}")
                }
            }
            Log.i(TAG, "Suppressed negative license result for package $packageName")

        }

        /**
         * Checks for license on all accounts.
         *
         * @return `null` if no check is performed (for example, because the feature is disabled),
         * an instance of [LicenseResponse] otherwise.
         */
        suspend fun checkLicenseCommon(
            packageName: String,
            request: LicenseRequestParameters
        ): LicenseResponse? {
            val callingUid = getCallingUid()

            if (!isLicensingEnabled(this@LicensingService)) {
                Log.d(TAG, "not checking license, as it is disabled by user")
                return@runBlocking
                return null
            }

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

            // 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 accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)
            val packageManager = packageManager

            lateinit var lastRespone: LicenseResponse
            if (accounts.isEmpty()) {
                handleNoAccounts(packageName, packageManager)
                return@runBlocking
                return null
            } else for (account: Account in accounts) {

                val response = httpClient.checkLicense(
                    account,
                    accountManager,
                    androidId,
                    packageName,
                    callingUid,
                    packageManager,
                    V2Request
                lastRespone = httpClient.checkLicense(
                    account, accountManager, androidId, packageInfo, packageName, request
                )

                if (response.result == LICENSED && response is V2Response) {
                    val bundle = Bundle()
                    bundle.putString(KEY_V2_RESULT_JWT, response.jwt)

                    try {
                        listener.verifyLicense(response.result, bundle)
                    } catch (e: Exception) {
                        Log.w(TAG, "Remote threw an exception while returning license result ${response}")
                    }
                if (lastRespone.result == LICENSED) {
                    return lastRespone;
                }
            }

            /*
             * Suppress failures on V2. V2 is commonly used by free apps whose checker
             * will not throw users out of the app if it never receives a response.
             *
             * This means that users who are signed in to a Google account will not
             * get a worse experience in these apps than users that are not signed in.
             *
             * Normally, we would otherwise send the response NOT_LICENSED with an empty
             * bundle here.
             */
            Log.i(TAG, "Suppressed negative license result for package $packageName")
            // Attempt to acquire license if app is free ("auto-purchase")
            val firstAccount = accounts[0]
            /* TODO if (acquireFreeAppLicense(firstAccount)) {
                lastRespone = httpClient.checkLicense(
                    firstAccount, accountManager, androidId, packageInfo, packageName, request
                )
            }*/

            return lastRespone
        }

        private fun handleNoAccounts(packageName: String, packageManager: PackageManager) {