Loading vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt +42 −29 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading @@ -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) { Loading @@ -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 Loading vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.kt→vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt +1 −29 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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( Loading vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt +75 −64 Original line number Diff line number Diff line Loading @@ -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}") } } Loading @@ -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) { Loading Loading
vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt +42 −29 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading @@ -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) { Loading @@ -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 Loading
vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.kt→vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt +1 −29 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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( Loading
vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt +75 −64 Original line number Diff line number Diff line Loading @@ -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}") } } Loading @@ -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) { Loading