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

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

Licensing service

parent ccb26ad5
Loading
Loading
Loading
Loading
+22 −2
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,18 +30,36 @@ android {
        }
    }

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


    buildFeatures {
        aidl = true
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
}

dependencies {
    implementation project(':fake-signature')

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

wire {
    kotlin {
        javaInterop = true
    }
}

if (file('user.gradle').exists()) {
+2 −0
Original line number Diff line number Diff line
@@ -5,6 +5,8 @@

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

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

    <permission
        android:name="com.android.vending.CHECK_LICENSE"
        android:protectionLevel="normal" />
+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];
        }
    }
}
+215 −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 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, Response.Listener<T> successListener, Response.ErrorListener errorListener) {
        super(GET, url, errorListener);

        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 ya29.[…]]",
            "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, 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,
                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, int versionCode, Response.Listener<String> successListener,
                  Response.ErrorListener errorListener) {
            super(
                "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode,
                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"));
            }
        }
    }
}
+149 −4
Original line number Diff line number Diff line
@@ -7,30 +7,175 @@ package com.android.vending.licensing;

import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
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;

public class LicensingService extends Service {
    private static final String TAG = "FakeLicenseService";
    private RequestQueue queue;

    private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA";

    private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() {




        /* 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;

        @Override
        public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException {
            Log.d(TAG, "checkLicense(" + nonce + ", " + packageName + ")");
            // We don't return anything yet. Seems to work good for some checkers.
            Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")");
            try {
                PackageInfo packageInfo = getPackageManager().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 {
                    Request request =
                        new LicenseRequest.V1(packageName, versionCode, nonce, data -> {
                            try {
                                if (data != null) {
                                    Log.v(TAG, "licenseV1 result was " + data.result + "with signed data " + data.signedData);

                                    if (data.result != null) {
                                        listener.verifyLicense(data.result, data.signedData, data.signature);
                                    } else {
                                        listener.verifyLicense(LICENSED, data.signedData, data.signature);
                                    }
                                } else {
                                    Log.v(TAG, "licenseV1 result was that user has no license");
                                    listener.verifyLicense(NOT_LICENSED, null, null);
                                }
                            } catch (RemoteException e) {
                                Log.e(TAG, "After returning licenseV1 result, remote threw an Exception.");
                                e.printStackTrace();
                            }
                        }, error -> {
                            Log.e(TAG, "licenseV1 request failed with " + error.toString());
                            try {
                                listener.verifyLicense(ERROR_CONTACTING_SERVER, null, null);
                            } catch (RemoteException e) {
                                Log.e(TAG, "After telling it that licenseV1 had an error when contacting server, remote threw an Exception.");
                                e.printStackTrace();
                                Log.e(TAG, "Caused after network error:");
                                error.printStackTrace();
                            }
                        });

                    request.setShouldCache(false);
                    queue.add(request);
                }
            } 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.d(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")");
            // We don't return anything yet. Seems to work good for some checkers.
            Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")");

            try {
                PackageInfo packageInfo = getPackageManager().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() + ")");
                    listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle());
                } else {
                    Request request =
                        new LicenseRequest.V2(packageName, versionCode, jwt -> {
                            Log.v(TAG, "LicenseV2 returned JWT license value " + jwt);
                            Bundle bundle = new Bundle();
                            bundle.putString(KEY_V2_RESULT_JWT, jwt);
                            try {
                                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());
                            try {
                                listener.verifyLicense(ERROR_CONTACTING_SERVER, new Bundle());
                            } catch (RemoteException e) {
                                Log.e(TAG, "After telling it that licenseV2 had an error when contacting server, remote threw an Exception.");
                                e.printStackTrace();
                                Log.e(TAG, "Caused after network error:");
                                error.printStackTrace();
                            }
                        });

                    request.setShouldCache(false);
                    queue.add(request);
                }
            } 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());
            }

        }
    };

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


}
Loading