From 44f9b0c414376779e7131255f30dd6e765ebc4fc Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 13:51:02 +0200 Subject: [PATCH] Query licenses from multiple Google accounts in a row --- .../vending/licensing/LicenseChecker.java | 202 ++++++++++++++ .../vending/licensing/LicensingService.java | 259 +++++------------- 2 files changed, 271 insertions(+), 190 deletions(-) create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java new file mode 100644 index 0000000000..bb8ed02731 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,202 @@ +package com.android.vending.licensing; + +import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static android.os.Binder.getCallingUid; + + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.util.Log; + +import com.android.vending.V1Container; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; + +import java.io.IOException; + +import kotlin.Unit; + +/** + * Performs license check including caller UID verification, using a given account, for which + * an auth token is fetched. + * + * @param Request parameter data value type + * @param Result type + */ +public abstract class LicenseChecker { + + private static final String TAG = "FakeLicenseChecker"; + + /* Possible response codes for checkLicense v1, from + * https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and + * the LVL library. + */ + + /** + * The application is licensed to the user. The user has purchased the application, or is authorized to + * download and install the alpha or beta version of the application. + */ + static final int LICENSED = 0x0; + /** + * The application is licensed to the user, but there is an updated application version available that is + * signed with a different key. + */ + static final int NOT_LICENSED = 0x1; + /** + * The application is not licensed to the user. + */ + static final int LICENSED_OLD_KEY = 0x2; + /** + * Server error — the application (package name) was not recognized by Google Play. + */ + static final int ERROR_NOT_MARKET_MANAGED = 0x3; + /** + * Server error — the server could not load the application's key pair for licensing. + */ + static final int ERROR_SERVER_FAILURE = 0x4; + static final int ERROR_OVER_QUOTA = 0x5; + + /** + * Local error — the Google Play application was not able to reach the licensing server, possibly because + * of network availability problems. + */ + static final int ERROR_CONTACTING_SERVER = 0x101; + /** + * Local error — the application requested a license check for a package that is not installed on the device. + */ + static final int ERROR_INVALID_PACKAGE_NAME = 0x102; + /** + * Local error — the application requested a license check for a package whose UID (package, user ID pair) + * does not match that of the requesting application. + */ + static final int ERROR_NON_MATCHING_UID = 0x103; + + static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; + + public void checkLicense(Account account, AccountManager accountManager, + String packageName, PackageManager packageManager, + RequestQueue queue, D queryData, + BiConsumerWithException onResult) + throws RemoteException { + try { + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + int versionCode = packageInfo.versionCode; + + // Verify caller identity + if (packageInfo.applicationInfo.uid != getCallingUid()) { + Log.e(TAG, + "an app illegally tried to request licenses for another app (caller: " + getCallingUid() + ")"); + onResult.accept(ERROR_NON_MATCHING_UID, null); + } else { + + accountManager.getAuthToken( + account, AUTH_TOKEN_SCOPE, false, + future -> { + try { + String auth = future.getResult().getString(KEY_AUTHTOKEN); + Request request = createRequest(packageName, auth, + versionCode, queryData, (Integer integer, R r) -> { + + try { + onResult.accept(integer, r); + } catch (RemoteException e) { + Log.e(TAG, + "After telling it the license check result, remote threw an Exception."); + e.printStackTrace(); + } + }, error -> { + Log.e(TAG, "license request failed with " + error.toString()); + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + }); + request.setShouldCache(false); + queue.add(request); + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + } + + }, null); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "an app tried to request licenses for package " + packageName + ", which does not exist"); + onResult.accept(ERROR_INVALID_PACKAGE_NAME, null); + } + } + + private static void safeSendResult( + BiConsumerWithException consumerWithException, A a, B b) { + try { + consumerWithException.accept(a, b); + } catch (Exception e) { + Log.e(TAG, "While sending result " + a + ", " + b + ", remote encountered an exception."); + e.printStackTrace(); + } + } + + public abstract Request createRequest(String packageName, String auth, int versionCode, D data, BiConsumer then, Response.ErrorListener errorListener); + + // Functional interfaces + + interface BiConsumerWithException { + void accept(A a, B b) throws T; + } + + interface BiConsumer { + void accept(A a, B b); + } + + static class Tuple { + public final A a; + public final B b; + + public Tuple(A a, B b) { + this.a = a; + this.b = b; + } + } + + // Implementations + + public static class V1 extends LicenseChecker> { + + @Override + public Request createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> then, + Response.ErrorListener errorListener) { + return new LicenseRequest.V1( + packageName, auth, versionCode, nonce, response -> { + if (response != null) { + Log.v(TAG, "licenseV1 result was " + response.result + " with signed data " + + response.signedData); + + if (response.result != null) { + then.accept(response.result, new Tuple<>(response.signedData, response.signature)); + } else { + then.accept(LICENSED, new Tuple<>(response.signedData, response.signature)); + } + } + }, errorListener + ); + } + } + + public static class V2 extends LicenseChecker { + @Override + public Request createRequest(String packageName, String auth, int versionCode, Unit data, + BiConsumer then, Response.ErrorListener errorListener) { + return new LicenseRequest.V2( + packageName, auth, versionCode, response -> { + if (response != null) { + then.accept(LICENSED, response); + } else { + then.accept(NOT_LICENSED, null); + } + }, errorListener + ); + } + } +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 1324b491b2..8b58ca22ed 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -5,12 +5,10 @@ package com.android.vending.licensing; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static com.android.vending.licensing.LicenseChecker.LICENSED; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.app.Service; import android.content.Intent; import android.content.pm.PackageInfo; @@ -20,14 +18,16 @@ import android.os.RemoteException; import android.util.Log; -import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; import org.microg.gms.auth.AuthConstants; -import java.io.IOException; -import java.util.concurrent.TimeUnit; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; + +import kotlin.Unit; public class LicensingService extends Service { private static final String TAG = "FakeLicenseService"; @@ -37,51 +37,6 @@ public class LicensingService extends Service { private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; - private static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; - - /* Possible response codes for checkLicense v1, from - * https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and - * the LVL library. - */ - - /** - * The application is licensed to the user. The user has purchased the application, or is authorized to - * download and install the alpha or beta version of the application. - */ - private static final int LICENSED = 0x0; - /** - * The application is licensed to the user, but there is an updated application version available that is - * signed with a different key. - */ - private static final int NOT_LICENSED = 0x1; - /** - * The application is not licensed to the user. - */ - private static final int LICENSED_OLD_KEY = 0x2; - /** - * Server error — the application (package name) was not recognized by Google Play. - */ - private static final int ERROR_NOT_MARKET_MANAGED = 0x3; - /** - * Server error — the server could not load the application's key pair for licensing. - */ - private static final int ERROR_SERVER_FAILURE = 0x4; - private static final int ERROR_OVER_QUOTA = 0x5; - - /** - * Local error — the Google Play application was not able to reach the licensing server, possibly because - * of network availability problems. - */ - private static final int ERROR_CONTACTING_SERVER = 0x101; - /** - * Local error — the application requested a license check for a package that is not installed on the device. - */ - private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; - /** - * Local error — the application requested a license check for a package whose UID (package, user ID pair) - * does not match that of the requesting application. - */ - private static final int ERROR_NON_MATCHING_UID = 0x103; private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { @@ -89,164 +44,88 @@ public class LicensingService extends Service { @Override public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); - try { - PackageManager packageManager = getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); - int versionCode = packageInfo.versionCode; - - // Verify caller identity - if (packageInfo.applicationInfo.uid != getCallingUid()) { - Log.e(TAG, "an app illegally tried to request v1 licenses for another app (caller: " + getCallingUid() + ")"); - listener.verifyLicense(ERROR_NON_MATCHING_UID, null, null); - } else { - - Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); - if (accounts.length == 0) { - Log.e(TAG, "not checking license, as user is not signed in"); - notificationRunnable.callerPackageName = packageName; - notificationRunnable.callerUid = packageInfo.applicationInfo.uid; - notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); - notificationRunnable.run(); - } else accountManager.getAuthToken( - accounts[0], AUTH_TOKEN_SCOPE, false, - future -> { - Request request = null; - try { - request = new LicenseRequest.V1( - packageName, - future.getResult().getString(KEY_AUTHTOKEN), - versionCode, nonce, data -> { - if (data != null) { - Log.v(TAG, "licenseV1 result was " + data.result + " with signed data " + - data.signedData); + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); - try { - if (data.result != null) { - listener.verifyLicense(data.result, data.signedData, data.signature); - } else { - listener.verifyLicense(LICENSED, data.signedData, data.signature); - } - } catch (RemoteException e) { - Log.e(TAG, - "After telling it the licenseV1 result, remote threw an Exception."); - e.printStackTrace(); - } - } else { - Log.v(TAG, "licenseV1 result was that user has no license"); - sendError(listener, NOT_LICENSED); - } - }, error -> { - Log.e(TAG, "licenseV1 request failed with " + error.toString()); - sendError(listener, ERROR_CONTACTING_SERVER); - }); - } catch (AuthenticatorException | IOException | OperationCanceledException e) { - sendError(listener, ERROR_CONTACTING_SERVER); - } + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicense(nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts))); + } + } - request.setShouldCache(false); - queue.add(request); - }, null); + private void checkLicense(long nonce, String packageName, PackageManager packageManager, + ILicenseResultListener listener, Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V1().checkLicense( + remainingAccounts.poll(), accountManager, packageName, packageManager, + queue, nonce, + (responseCode, stringTuple) -> { + if (responseCode != LICENSED && !remainingAccounts.isEmpty()) { + checkLicense(nonce, packageName, packageManager, listener, remainingAccounts); + } else { + listener.verifyLicense(responseCode, stringTuple.a, stringTuple.b); + } } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); - listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, null, null); - } + ); } @Override public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); - try { - PackageManager packageManager = getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); - int versionCode = packageInfo.versionCode; - - // Verify caller identity - if (packageInfo.applicationInfo.uid != getCallingUid()) { - Log.e(TAG, "an app illegally tried to request v2 licenses for another app (caller: " + getCallingUid() + ")"); - /* This negative result is provided even if users are not signed in; we expect apps - * will usually behave correctly in practise so this will not prevent users from - * using the app. - */ - listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle()); - } else { - Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); - if (accounts.length == 0) { - Log.e(TAG, "not checking license, as user is not signed in"); - notificationRunnable.callerPackageName = packageName; - notificationRunnable.callerUid = packageInfo.applicationInfo.uid; - notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); - notificationRunnable.run(); - } else accountManager.getAuthToken( - accounts[0], AUTH_TOKEN_SCOPE, false, - future -> { - try { - Bundle result = future.getResult(10, TimeUnit.SECONDS); - String auth = result.getString(KEY_AUTHTOKEN); + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicenseV2(packageName, packageManager, listener, extraParams, new LinkedList<>(Arrays.asList(accounts))); + } + } - Request request = new LicenseRequest.V2(packageName, auth, versionCode, jwt -> { - Log.v(TAG, "LicenseV2 returned JWT license value " + jwt); - Bundle bundle = new Bundle(); - bundle.putString(KEY_V2_RESULT_JWT, jwt); - try { - if (jwt == null) { - /* - * 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. - */ - Log.i(TAG, "Suppressed negative result for package " + packageName); - } else { - listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); - } - } catch (RemoteException e) { - Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); - e.printStackTrace(); - } - }, error -> { - Log.e(TAG, "licenseV2 request failed with " + error.toString()); - //sendError(listener, ERROR_CONTACTING_SERVER); – see above - }); + private void checkLicenseV2(String packageName, PackageManager packageManager, + ILicenseV2ResultListener listener, Bundle extraParams, + Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V2().checkLicense( + remainingAccounts.poll(), accountManager, packageName, packageManager, queue, Unit.INSTANCE, + (responseCode, data) -> { + /* + * 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. + */ + if (responseCode == LICENSED) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_V2_RESULT_JWT, data); + + listener.verifyLicense(responseCode, bundle); + } else if (!remainingAccounts.isEmpty()) { + checkLicenseV2(packageName, packageManager, listener, extraParams, remainingAccounts); + } else { + Log.i(TAG, "Suppressed negative license result for package " + packageName); + } + } + ); - request.setShouldCache(false); - queue.add(request); + } - } catch (AuthenticatorException | IOException | OperationCanceledException e) { - //sendError(listener, ERROR_CONTACTING_SERVER); – see above - e.printStackTrace(); - } - }, null - ); - } + private void handleNoAccounts(String packageName, PackageManager packageManager) { + try { + Log.e(TAG, "not checking license, as user is not signed in"); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + notificationRunnable.callerUid = packageInfo.applicationInfo.uid; + notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); - listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, new Bundle()); + Log.e(TAG, "ignored license request, but package name " + packageName + " was not known!"); + notificationRunnable.callerAppName = packageName; } - + notificationRunnable.run(); } }; - private static void sendError(ILicenseResultListener listener, int error) { - try { - listener.verifyLicense(error, null, null); - } catch (RemoteException e) { - Log.e(TAG, "After telling it that licenseV1 had an error (" + error + "), remote threw an Exception."); - } - } - - private static void sendError(ILicenseV2ResultListener listener, int error) { - try { - listener.verifyLicense(error, new Bundle()); - } catch (RemoteException e) { - Log.e(TAG, "After telling it that licenseV2 had an error (" + error + "), remote threw an Exception."); - } - } - public IBinder onBind(Intent intent) { queue = Volley.newRequestQueue(this); accountManager = AccountManager.get(this);