Loading vending-app/src/main/AndroidManifest.xml +2 −2 Original line number Diff line number Diff line Loading @@ -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 Loading vending-app/src/main/java/com/android/vending/Util.kt +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() } Loading @@ -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 vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt +127 −188 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.kt +147 −204 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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") Loading @@ -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() Loading Loading @@ -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() ) Loading @@ -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") Loading @@ -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") } vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotification.kt +7 −6 Original line number Diff line number Diff line Loading @@ -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
vending-app/src/main/AndroidManifest.xml +2 −2 Original line number Diff line number Diff line Loading @@ -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 Loading
vending-app/src/main/java/com/android/vending/Util.kt +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() } Loading @@ -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
vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt +127 −188 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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)
vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.kt +147 −204 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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") Loading @@ -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() Loading Loading @@ -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() ) Loading @@ -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") Loading @@ -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") }
vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotification.kt +7 −6 Original line number Diff line number Diff line Loading @@ -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