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

Commit 44f9b0c4 authored by Fynn Godau's avatar Fynn Godau
Browse files

Query licenses from multiple Google accounts in a row

parent 59fab371
Loading
Loading
Loading
Loading
+202 −0
Original line number Diff line number Diff line
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 <D> Request parameter data value type
 * @param <R> Result type
 */
public abstract class LicenseChecker<D, R> {

    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<Integer, R, RemoteException> 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 <A, B, T extends Exception> void safeSendResult(
        BiConsumerWithException<A, B, T> 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<Integer, R> then, Response.ErrorListener errorListener);

    // Functional interfaces

    interface BiConsumerWithException<A, B, T extends Exception> {
        void accept(A a, B b) throws T;
    }

    interface BiConsumer<A, B> {
        void accept(A a, B b);
    }

    static class Tuple<A, B> {
        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<Long, Tuple<String, String>> {

        @Override
        public Request<V1Container> createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer<Integer, Tuple<String, String>> 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<Unit, String> {
        @Override
        public Request<String> createRequest(String packageName, String auth, int versionCode, Unit data,
                                             BiConsumer<Integer, String> 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
            );
        }
    }
}
+69 −190
Original line number Diff line number Diff line
@@ -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.IBinder;
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,110 +44,52 @@ 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);
            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 -> {
                            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);

                                        try {
                                            if (data.result != null) {
                                                listener.verifyLicense(data.result, data.signedData, data.signature);
                handleNoAccounts(packageName, packageManager);
            } else {
                                                listener.verifyLicense(LICENSED, data.signedData, data.signature);
                checkLicense(nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts)));
            }
                                        } 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);
        }

                            request.setShouldCache(false);
                            queue.add(request);
                        }, null);
        private void checkLicense(long nonce, String packageName, PackageManager packageManager,
                                  ILicenseResultListener listener, Queue<Account> 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);
            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);
                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) {
        private void checkLicenseV2(String packageName, PackageManager packageManager,
                                    ILicenseV2ResultListener listener, Bundle extraParams,
                                    Queue<Account> 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.
@@ -200,52 +97,34 @@ public class LicensingService extends Service {
                     * 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);
                    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 {
                                            listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle);
                                        }
                                    } catch (RemoteException e) {
                                        Log.e(TAG, "After returning licenseV2 result, remote threw an Exception.");
                                        e.printStackTrace();
                        Log.i(TAG, "Suppressed negative license result for package " + packageName);
                    }
                                }, error -> {
                                    Log.e(TAG, "licenseV2 request failed with " + error.toString());
                                    //sendError(listener, ERROR_CONTACTING_SERVER); – see above
                                });

                                request.setShouldCache(false);
                                queue.add(request);

                            } catch (AuthenticatorException | IOException | OperationCanceledException e) {
                                //sendError(listener, ERROR_CONTACTING_SERVER); – see above
                                e.printStackTrace();
                }
                        }, null
            );
                }
            } 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());
            }

        }
    };

    private static void sendError(ILicenseResultListener listener, int error) {
        private void handleNoAccounts(String packageName, PackageManager packageManager) {
            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.");
                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, "ignored license request, but package name " + packageName + " was not known!");
                notificationRunnable.callerAppName = packageName;
            }
            notificationRunnable.run();
        }
    };

    public IBinder onBind(Intent intent) {
        queue = Volley.newRequestQueue(this);