Loading vending-app/build.gradle +25 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ */ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'com.squareup.wire' android { namespace "com.android.vending" Loading @@ -28,6 +30,15 @@ android { } } sourceSets { main { java { srcDirs += "build/generated/source/proto/main/java" } } } buildFeatures { aidl = true } Loading @@ -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()) { Loading vending-app/src/main/AndroidManifest.xml +15 −0 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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" /> Loading @@ -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> vending-app/src/main/java/com/android/vending/Util.java 0 → 100644 +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]; } } } vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java 0 → 100644 +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 ); } } } vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java 0 → 100644 +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
vending-app/build.gradle +25 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ */ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'com.squareup.wire' android { namespace "com.android.vending" Loading @@ -28,6 +30,15 @@ android { } } sourceSets { main { java { srcDirs += "build/generated/source/proto/main/java" } } } buildFeatures { aidl = true } Loading @@ -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()) { Loading
vending-app/src/main/AndroidManifest.xml +15 −0 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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" /> Loading @@ -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>
vending-app/src/main/java/com/android/vending/Util.java 0 → 100644 +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]; } } }
vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java 0 → 100644 +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 ); } } }
vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java 0 → 100644 +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")); } } } }