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

Commit d769b7a9 authored by Fynn Godau's avatar Fynn Godau
Browse files

Merge licensing code into e's fork

parents 761089fc c11e4183
Loading
Loading
Loading
Loading
Loading
+25 −0
Original line number Diff line number Diff line
@@ -4,6 +4,8 @@
 */

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.squareup.wire'

android {
    namespace "com.android.vending"
@@ -28,6 +30,15 @@ android {
        }
    }

    sourceSets {
        main {
            java {
                srcDirs += "build/generated/source/proto/main/java"
            }
        }
    }


    buildFeatures {
        aidl = true
    }
@@ -36,10 +47,24 @@ android {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = 1.8
    }
}

dependencies {
    implementation project(':fake-signature')
    implementation project(':play-services-auth')

    implementation "com.squareup.wire:wire-runtime:$wireVersion"
    implementation "com.android.volley:volley:$volleyVersion"
}

wire {
    kotlin {
        javaInterop = true
    }
}

if (file('user.gradle').exists()) {
+15 −0
Original line number Diff line number Diff line
@@ -9,6 +9,14 @@
        android:name="com.android.vending.CHECK_LICENSE"
        android:protectionLevel="normal" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />

    <uses-permission
        android:name="android.permission.USE_CREDENTIALS"
        android:maxSdkVersion="22" />

    <application
        android:forceQueryable="true"
        android:icon="@mipmap/ic_app"
@@ -30,6 +38,7 @@

        <service
            android:name="com.android.vending.licensing.LicensingService"
            android:permission="com.android.vending.CHECK_LICENSE"
            android:exported="true">
            <intent-filter>
                <action android:name="com.android.vending.licensing.ILicensingService" />
@@ -55,5 +64,11 @@
                <category android:name="android.intent.category.INFO" />
            </intent-filter>
        </activity>

        <receiver android:name="com.android.vending.licensing.LicenseServiceNotificationRunnable$IgnoreReceiver"
            android:exported="false" />
        <receiver android:name="com.android.vending.licensing.LicenseServiceNotificationRunnable$SignInReceiver"
            android:exported="false" />

    </application>
</manifest>
+28 −0
Original line number Diff line number Diff line
package com.android.vending;

import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;

public class Util {

    private static final String TAG = "FakeStoreUtil";

    /**
     * From <a href="https://stackoverflow.com/a/46688434/">StackOverflow</a>, CC BY-SA 4.0 by Sergey Frolov, adapted.
     */
    public static byte[] encodeGzip(final byte[] input) {

        try (final ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
             final GZIPOutputStream gzipOutput = new GZIPOutputStream(byteOutput)) {
            gzipOutput.write(input);
            gzipOutput.finish();
            return byteOutput.toByteArray();
        } catch (IOException e) {
            Log.e(TAG, "Failed to encode bytes as GZIP");
            return new byte[0];
        }
    }
}
+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
            );
        }
    }
}
+217 −0
Original line number Diff line number Diff line
package com.android.vending.licensing;

import static com.android.volley.Request.Method.GET;

import android.util.Base64;
import android.util.Log;

import com.android.vending.AndroidVersionMeta;
import com.android.vending.DeviceMeta;
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;
import com.android.vending.Timestamp;
import com.android.vending.TimestampContainer;
import com.android.vending.TimestampContainer1;
import com.android.vending.TimestampContainer1Wrapper;
import com.android.vending.TimestampContainer2;
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 java.io.IOException;
import java.util.Map;
import java.util.UUID;

import okio.ByteString;

public abstract class LicenseRequest<T> extends Request<T> {

    private final String xPsRh;
    private final String auth;
    private static final String TAG = "FakeLicenseRequest";

    private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING;
    private static final long ANDROID_ID = 1;

    private final Response.Listener<T> successListener;


    protected LicenseRequest(String url, String auth, Response.Listener<T> successListener, Response.ErrorListener errorListener) {
        super(GET, url, errorListener);
        this.auth = auth;

        this.successListener = successListener;

        long millis = System.currentTimeMillis();
        TimestampContainer.Builder timestamp = new TimestampContainer.Builder()
            .container2(new TimestampContainer2.Builder()
                .wrapper(new TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build())
                .timestamp(makeTimestamp(millis))
                .build());
        millis = System.currentTimeMillis();
        timestamp
            .container1Wrapper(new TimestampContainer1Wrapper.Builder()
                .androidId(String.valueOf(ANDROID_ID))
                .container(new TimestampContainer1.Builder()
                    .timestamp(millis + "000")
                    .wrapper(makeTimestamp(millis))
                    .build())
                .build()
            );
        String encodedTimestamps = new String(
            Base64.encode(Util.encodeGzip(timestamp.build().encode()), BASE64_FLAGS)
        );

        Locality locality = new Locality.Builder()
            .unknown1(1)
            .unknown2(2)
            .countryCode("")
            .region(new TimestampStringWrapper.Builder()
                .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build())
            .country(new TimestampStringWrapper.Builder()
                .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build())
            .unknown3(0)
            .build();
        String encodedLocality = new String(
            Base64.encode(locality.encode(), BASE64_FLAGS)
        );

        byte[] header = new LicenseRequestHeader.Builder()
            .encodedTimestamps(new StringWrapper.Builder().string(encodedTimestamps).build())
            .triple(
                new EncodedTripleWrapper.Builder().triple(
                    new EncodedTriple.Builder()
                        .encoded1("")
                        .encoded2("")
                        .empty("")
                        .build()
                ).build()
            )
            .locality(new LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build())
            .unknown(new IntWrapper.Builder().integer(5).build())
            .empty("")
            .deviceMeta(new DeviceMeta.Builder()
                .android(
                    new AndroidVersionMeta.Builder()
                        .androidSdk(0)
                        .buildNumber("")
                        .androidVersion("")
                        .unknown(0)
                        .build()
                )
                .unknown1(new UnknownByte12.Builder().bytes(new ByteString(new byte[]{}
                )).build())
                .unknown2(1)
                .build()
            )
            .userAgent(new UserAgent.Builder()
                .deviceProductName("")
                .deviceSoc("")
                .deviceModelName("")
                .finskyVersion("")
                .deviceName("")
                .androidId(ANDROID_ID) // must not be 0
                .deviceSignature("")
                .build()
            )
            .uuid(new Uuid.Builder()
                .uuid(UUID.randomUUID().toString())
                .unknown(2)
                .build()
            )
            .build().encode();
        this.xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS));

        //Log.d(TAG, "Product " + Build.PRODUCT + ", Board " + Build.BOARD + " Model " +  Build.MODEL + " Device " + Build.DEVICE);

        Log.v(TAG, "X-PS-RH: " + xPsRh);
    }

    @Override
    public Map<String, String> getHeaders() {
        return Map.of(
            "X-PS-RH", xPsRh,
            "Authorization", "Bearer " + auth,
            "Connection", "Keep-Alive"
        );
    }

    @Override
    protected void deliverResponse(T response) {
        successListener.onResponse(response);
    }

    private static Timestamp makeTimestamp(long millis) {
        return new Timestamp.Builder()
            .seconds((int) (millis / 1000))
            .nanos(Math.floorMod(millis, 1000) * 1000000)
            .build();
    }

    public static class V1 extends LicenseRequest<V1Container> {

        public V1(String packageName, String auth, int versionCode, long nonce, Response.Listener<V1Container> successListener, Response.ErrorListener errorListener) {
            super("https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=" + packageName + "&vc=" + versionCode + "&nnc=" + nonce,
                auth, successListener, errorListener
            );
        }

        @Override
        protected Response<V1Container> parseNetworkResponse(NetworkResponse response) {
            if (response != null && response.data != null) {
                try {
                    LicenseResult result = LicenseResult.ADAPTER.decode(response.data);
                    return Response.success(result.information.v1, null);
                } catch (IOException e) {
                    return Response.error(new VolleyError(e));
                } catch (NullPointerException e) {
                    // A field does not exist → user has no license
                    return Response.success(null, null);
                }
            } else {
                return Response.error(new VolleyError("No response was returned"));
            }
        }
    }

    public static class V2 extends LicenseRequest<String> {
        public V2(String packageName, String auth, int versionCode, Response.Listener<String> successListener,
                  Response.ErrorListener errorListener) {
            super(
                "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode,
                auth, successListener, errorListener
            );
        }

        @Override
        protected Response<String> parseNetworkResponse(NetworkResponse response) {
            if (response != null && response.data != null) {
                try {
                    LicenseResult result = LicenseResult.ADAPTER.decode(response.data);
                    return Response.success(result.information.v2.license.jwt, null);
                } catch (IOException e) {
                    return Response.error(new VolleyError(e));
                } catch (NullPointerException e) {
                    // A field does not exist → user has no license
                    return Response.success(null, null);
                }
            } else {
                return Response.error(new VolleyError("No response was returned"));
            }
        }
    }
}
Loading