From 35f71f350aced519e910f6729ee5b182965e12f9 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 26 Sep 2023 20:00:27 +0200 Subject: [PATCH 01/11] Licensing service --- vending-app/build.gradle | 24 +- vending-app/src/main/AndroidManifest.xml | 2 + .../main/java/com/android/vending/Util.java | 28 +++ .../vending/licensing/LicenseRequest.java | 215 ++++++++++++++++++ .../vending/licensing/LicensingService.java | 153 ++++++++++++- .../src/main/proto/LicenseRequest.proto | 71 ++++++ .../src/main/proto/LicenseResult.proto | 28 +++ vending-app/src/main/proto/Locality.proto | 21 ++ vending-app/src/main/proto/Timestamp.proto | 33 +++ 9 files changed, 569 insertions(+), 6 deletions(-) create mode 100644 vending-app/src/main/java/com/android/vending/Util.java create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java create mode 100644 vending-app/src/main/proto/LicenseRequest.proto create mode 100644 vending-app/src/main/proto/LicenseResult.proto create mode 100644 vending-app/src/main/proto/Locality.proto create mode 100644 vending-app/src/main/proto/Timestamp.proto diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 24ef7bd3e..fd128f029 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -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()) { diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 2a7887045..81d4ca323 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ + + diff --git a/vending-app/src/main/java/com/android/vending/Util.java b/vending-app/src/main/java/com/android/vending/Util.java new file mode 100644 index 000000000..0585a28e3 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/Util.java @@ -0,0 +1,28 @@ +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 StackOverflow, 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]; + } + } +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java new file mode 100644 index 000000000..60742a1d7 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java @@ -0,0 +1,215 @@ +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 extends Request { + + 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 successListener; + + + protected LicenseRequest(String url, Response.Listener 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 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 { + + public V1(String packageName, int versionCode, long nonce, Response.Listener successListener, Response.ErrorListener errorListener) { + super("https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=" + packageName + "&vc=" + versionCode + "&nnc=" + nonce, + successListener, errorListener + ); + } + + @Override + protected Response 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 { + public V2(String packageName, int versionCode, Response.Listener successListener, + Response.ErrorListener errorListener) { + super( + "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode, + successListener, errorListener + ); + } + + @Override + protected Response 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")); + } + } + } +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index b479aab64..ba351bf8c 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -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; } + + } diff --git a/vending-app/src/main/proto/LicenseRequest.proto b/vending-app/src/main/proto/LicenseRequest.proto new file mode 100644 index 000000000..da8747c2c --- /dev/null +++ b/vending-app/src/main/proto/LicenseRequest.proto @@ -0,0 +1,71 @@ +syntax = "proto2"; + +option java_package = "com.android.vending"; +option java_multiple_files = true; + +message LicenseRequestHeader { + optional StringWrapper encodedTimestamps = 1; + optional EncodedTripleWrapper triple = 10; + optional LocalityWrapper locality = 11; + optional IntWrapper unknown = 12; + optional string empty = 14; + optional DeviceMeta deviceMeta = 20; + optional UserAgent userAgent = 21; + optional Uuid uuid = 27; +} + +message StringWrapper { + optional string string = 1; +} + +message EncodedTripleWrapper { + optional EncodedTriple triple = 1; +} + +message EncodedTriple { + optional string encoded1 = 1; + optional string encoded2 = 2; + optional string empty = 3; +} + +message LocalityWrapper { + optional string encodedLocalityProto = 1; +} + +message IntWrapper { + optional uint32 integer = 1; +} + +message DeviceMeta { + optional AndroidVersionMeta android = 1; + optional UnknownByte12 unknown1 = 2; + optional uint32 unknown2 = 3; // observed value: 1 + +} + +message AndroidVersionMeta { + optional uint32 androidSdk = 1; + optional string buildNumber = 2; + optional string androidVersion = 3; + optional uint32 unknown = 4; +} + +message UnknownByte12 { + optional bytes bytes = 12; +} + +message UserAgent { + // The names of these attributes are vague guesses and should be adapted if needed. + optional string deviceProductName = 1; // e.g. "OnePlusNord" + optional string deviceSoc = 2; // e.g. "qcom" + optional string deviceModelName = 3; // e.g. "OnePlus Nord" + optional string finskyVersion = 4; // e.g. "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" + optional string deviceName = 5; // e.g. "OnePlusNord"; difference to 1 not yet clear + optional uint64 androidId = 6; + optional string deviceSignature = 7; // e.g. "google/walleye/walleye:8.1.0/OPM1.171019.011/4448085:user/release-keys" +} + +message Uuid { + optional string uuid = 1; + optional uint32 unknown = 2; // might be a constant, e.g. format ID. Observed value: 2. +} diff --git a/vending-app/src/main/proto/LicenseResult.proto b/vending-app/src/main/proto/LicenseResult.proto new file mode 100644 index 000000000..238aa263f --- /dev/null +++ b/vending-app/src/main/proto/LicenseResult.proto @@ -0,0 +1,28 @@ +syntax = "proto2"; + +option java_package = "com.android.vending"; +option java_multiple_files = true; + +message LicenseResult { + optional LicenseInformation information = 1; +} + +message LicenseInformation { + optional V1Container v1 = 76; + optional V2Container v2 = 173; +} + +message V1Container { + optional uint32 result = 1; + optional string signedData = 2; + optional string signature = 3; + +} + +message V2Container { + optional AppLicense license = 1; +} + +message AppLicense { + optional string jwt = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/proto/Locality.proto b/vending-app/src/main/proto/Locality.proto new file mode 100644 index 000000000..8e74fbcf6 --- /dev/null +++ b/vending-app/src/main/proto/Locality.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +option java_package = "com.android.vending"; +option java_multiple_files = true; + +import "Timestamp.proto"; + +message Locality { + optional uint32 unknown1 = 2; // value: 1 + optional uint32 unknown2 = 3; // value: 0 + optional string countryCode = 4; // e.g. "DE" + optional TimestampStringWrapper region = 8; // e.g. "DE-BY" and a timestamp + optional TimestampStringWrapper country = 9; // e.g. "DE" and a timestamp + optional uint32 unknown3 = 11; // value: 0 + +} + +message TimestampStringWrapper { + optional string string = 1; + optional Timestamp timestamp = 2; +} diff --git a/vending-app/src/main/proto/Timestamp.proto b/vending-app/src/main/proto/Timestamp.proto new file mode 100644 index 000000000..9459ff827 --- /dev/null +++ b/vending-app/src/main/proto/Timestamp.proto @@ -0,0 +1,33 @@ +syntax = "proto2"; + +option java_package = "com.android.vending"; +option java_multiple_files = true; + +message TimestampContainer { + optional TimestampContainer1Wrapper container1Wrapper = 3; + optional TimestampContainer2 container2 = 6; +} + +message TimestampContainer1Wrapper { + optional string androidId = 1; + optional TimestampContainer1 container = 2; +} + +message TimestampContainer1 { + optional string timestamp = 1; + optional Timestamp wrapper = 2; +} + +message Timestamp { + optional uint32 seconds = 1; + optional uint32 nanos = 2; +} + +message TimestampContainer2 { + optional TimestampWrapper wrapper = 1; + optional Timestamp timestamp = 2; +} + +message TimestampWrapper { + optional Timestamp timestamp = 1; +} \ No newline at end of file -- GitLab From cbda3bcb1b5fbad11ba9f07cd2302e9ced0aec69 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 1 Oct 2023 18:43:25 +0200 Subject: [PATCH 02/11] Replace hardcoded auth token with auth token fetched via account --- vending-app/build.gradle | 1 + vending-app/src/main/AndroidManifest.xml | 11 +- .../vending/licensing/LicenseRequest.java | 14 +- .../vending/licensing/LicensingService.java | 243 +++++++++++------- 4 files changed, 165 insertions(+), 104 deletions(-) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index fd128f029..ade98d48f 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -51,6 +51,7 @@ android { dependencies { implementation project(':fake-signature') + implementation project(':play-services-auth') implementation "com.squareup.wire:wire-runtime:$wireVersion" implementation "com.android.volley:volley:$volleyVersion" diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 81d4ca323..aa713828a 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -5,12 +5,19 @@ - - + + + + + extends Request { 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; @@ -49,8 +50,9 @@ public abstract class LicenseRequest extends Request { private final Response.Listener successListener; - protected LicenseRequest(String url, Response.Listener successListener, Response.ErrorListener errorListener) { + protected LicenseRequest(String url, String auth, Response.Listener successListener, Response.ErrorListener errorListener) { super(GET, url, errorListener); + this.auth = auth; this.successListener = successListener; @@ -143,7 +145,7 @@ public abstract class LicenseRequest extends Request { public Map getHeaders() { return Map.of( "X-PS-RH", xPsRh, - "Authorization", "Bearer ya29.[…]]", + "Authorization", "Bearer " + auth, "Connection", "Keep-Alive" ); } @@ -162,9 +164,9 @@ public abstract class LicenseRequest extends Request { public static class V1 extends LicenseRequest { - public V1(String packageName, int versionCode, long nonce, Response.Listener successListener, Response.ErrorListener errorListener) { + public V1(String packageName, String auth, int versionCode, long nonce, Response.Listener successListener, Response.ErrorListener errorListener) { super("https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=" + packageName + "&vc=" + versionCode + "&nnc=" + nonce, - successListener, errorListener + auth, successListener, errorListener ); } @@ -187,11 +189,11 @@ public abstract class LicenseRequest extends Request { } public static class V2 extends LicenseRequest { - public V2(String packageName, int versionCode, Response.Listener successListener, + public V2(String packageName, String auth, int versionCode, Response.Listener successListener, Response.ErrorListener errorListener) { super( "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode, - successListener, errorListener + auth, successListener, errorListener ); } diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index ba351bf8c..267ddc84b 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -5,6 +5,12 @@ package com.android.vending.licensing; +import static android.accounts.AccountManager.KEY_AUTHTOKEN; + +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; @@ -18,60 +24,66 @@ 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; + public class LicensingService extends Service { private static final String TAG = "FakeLicenseService"; private RequestQueue queue; + private AccountManager accountManager; private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; - private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { - - + 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() { - /* 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 { @@ -85,39 +97,50 @@ public class LicensingService extends Service { 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); + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + + if (accounts.length == 0) { + Log.e(TAG, "not checking license, as user is not signed in"); + } 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); + } else { + listener.verifyLicense(LICENSED, data.signedData, data.signature); + } + } catch (RemoteException e) { + Log.e(TAG, + "After telling it the licenseV1 result, remote threw an Exception."); + e.printStackTrace(); + } } else { - listener.verifyLicense(LICENSED, data.signedData, data.signature); + Log.v(TAG, "licenseV1 result was that user has no license"); + sendError(listener, NOT_LICENSED); } - } 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()); + sendError(listener, ERROR_CONTACTING_SERVER); + }); + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + sendError(listener, ERROR_CONTACTING_SERVER); } - }, 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); + request.setShouldCache(false); + queue.add(request); + }, null); } } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); @@ -138,31 +161,41 @@ public class LicensingService extends Service { 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()); + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + + if (accounts.length == 0) { + Log.e(TAG, "not checking license, as user is not signed in"); + } else accountManager.getAuthToken( + accounts[0], AUTH_TOKEN_SCOPE, false, + future -> { 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."); + Bundle result = future.getResult(10, TimeUnit.SECONDS); + String auth = result.getString(KEY_AUTHTOKEN); + + 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 { + listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); + } catch (RemoteException e) { + Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); + e.printStackTrace(); + } + }, error -> { + Log.e(TAG, "licenseV2 request failed with " + error.toString()); + sendError(listener, ERROR_CONTACTING_SERVER); + }); + + request.setShouldCache(false); + queue.add(request); + + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + sendError(listener, ERROR_CONTACTING_SERVER); e.printStackTrace(); - Log.e(TAG, "Caused after network error:"); - error.printStackTrace(); } - }); - - request.setShouldCache(false); - queue.add(request); + }, null + ); } } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); @@ -172,8 +205,26 @@ public class LicensingService extends Service { } }; + private static void sendError(ILicenseResultListener listener, int error) { + try { + listener.verifyLicense(error, null, null); + } catch (RemoteException e) { + Log.e(TAG, "After telling it that licenseV1 had an error (" + error + "), remote threw an Exception."); + } + } + + private static void sendError(ILicenseV2ResultListener listener, int error) { + try { + listener.verifyLicense(error, new Bundle()); + } catch (RemoteException e) { + Log.e(TAG, "After telling it that licenseV2 had an error (" + error + "), remote threw an Exception."); + } + } + public IBinder onBind(Intent intent) { queue = Volley.newRequestQueue(this); + accountManager = AccountManager.get(this); + return mLicenseService; } -- GitLab From 85875d1f11c55536367f0d0ee2df46abae59ff88 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 3 Oct 2023 16:37:23 +0200 Subject: [PATCH 03/11] Notify is user not signed in to any Google account --- vending-app/src/main/AndroidManifest.xml | 7 + .../LicenseServiceNotificationRunnable.java | 157 ++++++++++++++++++ .../vending/licensing/LicensingService.java | 16 +- vending-app/src/main/res/values/strings.xml | 8 + 4 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index aa713828a..ad6dfacd8 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ android:protectionLevel="normal" /> + + + + + diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java new file mode 100644 index 000000000..f692c413f --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -0,0 +1,157 @@ +package com.android.vending.licensing; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.CallSuper; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.android.vending.R; + +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +public class LicenseServiceNotificationRunnable implements Runnable { + + private final Context context; + + public String callerPackageName; + public CharSequence callerAppName; + public int callerUid; + + private static final String TAG = "FakeLicenseNotification"; + private static final String GMS_PACKAGE_NAME = "com.google.android.gms"; + private static final String GMS_AUTH_INTENT_ACTION = "com.google.android.gms.auth.login.LOGIN"; + + private static final String PREFERENCES_KEY_IGNORE_PACKAGES_LIST = "ignorePackages"; + private static final String PREFERENCES_FILE_NAME = "licensing"; + + private static final String INTENT_KEY_IGNORE_PACKAGE_NAME = "package"; + private static final String INTENT_KEY_NOTIFICATION_ID = "id"; + + + public LicenseServiceNotificationRunnable(Context context) { + this.context = context; + } + + private static final String CHANNEL_ID = "LicenseNotification"; + + @Override + public void run() { + registerNotificationChannel(); + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE); + + Set ignoreList = preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, Collections.emptySet()); + for (String ignoredPackage : ignoreList) { + if (callerPackageName.equals(ignoredPackage)) { + Log.d(TAG, "Not notifying about license check, as user has ignored notifications for package " + ignoredPackage); + return; + } + } + + Intent authIntent = new Intent(context, LicenseServiceNotificationRunnable.SignInReceiver.class); + authIntent.putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid); + PendingIntent authPendingIntent = PendingIntent.getBroadcast( + context, callerUid * 2, authIntent, PendingIntent.FLAG_IMMUTABLE + ); + + Intent ignoreIntent = new Intent(context, LicenseServiceNotificationRunnable.IgnoreReceiver.class); + ignoreIntent.putExtra(INTENT_KEY_IGNORE_PACKAGE_NAME, callerPackageName); + ignoreIntent.putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid); + PendingIntent ignorePendingIntent = PendingIntent.getBroadcast( + context, callerUid * 2 + 1, ignoreIntent, PendingIntent.FLAG_IMMUTABLE + ); + + Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_app_foreground) + .setContentTitle(context.getString(R.string.license_notification_title, callerAppName)) + .setContentText(context.getString(R.string.license_notification_body)) + .addAction( + new NotificationCompat.Action.Builder( + null, context.getString(R.string.license_notification_sign_in), authPendingIntent + ).build() + ) + .addAction( + new NotificationCompat.Action.Builder( + null, context.getString(R.string.license_notification_ignore), ignorePendingIntent + ).build() + ) + .setAutoCancel(true) + .build(); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.notify(callerUid, notification); + + } + + private void registerNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + context.getString(R.string.license_notification_channel_name), + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription(context.getString(R.string.license_notification_channel_description)); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + } + + private static class Receiver extends BroadcastReceiver { + + @Override + @CallSuper + public void onReceive(Context context, Intent intent) { + // Dismiss notification + NotificationManagerCompat.from(context) + .cancel(intent.getIntExtra(INTENT_KEY_NOTIFICATION_ID, -1)); + } + } + + public static final class IgnoreReceiver extends Receiver { + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE); + + Set ignoreList = new TreeSet<>( + preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, Collections.emptySet()) + ); + + String newIgnorePackage = intent.getStringExtra(INTENT_KEY_IGNORE_PACKAGE_NAME); + Log.d(TAG, "Adding package " + newIgnorePackage + " to ignore list"); + + ignoreList.add(newIgnorePackage); + preferences.edit().putStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, ignoreList).apply(); + } + } + + public static final class SignInReceiver extends Receiver { + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + + Log.d(TAG, "Starting sign in activity"); + Intent authIntent = new Intent(GMS_AUTH_INTENT_ACTION); + authIntent.setPackage(GMS_PACKAGE_NAME); + authIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(authIntent); + } + } + +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 267ddc84b..f6df3b71e 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -33,6 +33,7 @@ public class LicensingService extends Service { private static final String TAG = "FakeLicenseService"; private RequestQueue queue; private AccountManager accountManager; + private LicenseServiceNotificationRunnable notificationRunnable; private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; @@ -89,7 +90,8 @@ public class LicensingService extends Service { public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); try { - PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + PackageManager packageManager = getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); int versionCode = packageInfo.versionCode; // Verify caller identity @@ -102,6 +104,10 @@ public class LicensingService extends Service { 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 -> { @@ -153,7 +159,8 @@ public class LicensingService extends Service { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); try { - PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + PackageManager packageManager = getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); int versionCode = packageInfo.versionCode; // Verify caller identity @@ -165,6 +172,10 @@ public class LicensingService extends Service { 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 -> { @@ -224,6 +235,7 @@ public class LicensingService extends Service { public IBinder onBind(Intent intent) { queue = Volley.newRequestQueue(this); accountManager = AccountManager.get(this); + notificationRunnable = new LicenseServiceNotificationRunnable(this); return mLicenseService; } diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index 516911986..07c309f95 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -8,4 +8,12 @@ microG Companion microG Companion cannot be used standalone. Opened microG Services settings instead. microG Companion cannot be used standalone. Please install microG Services to use microG. + + License notifications + Notifies when an app tries to validate its license, but you are not signed in to any Google account. + %1$s could not verify license + If the app is misbehaving, sign in to a Google account with which you have bought the app. + + Sign In + Ignore \ No newline at end of file -- GitLab From 0d4ccc60e734a73730bddc01ace70ba27f1a45c1 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 3 Oct 2023 16:42:23 +0200 Subject: [PATCH 04/11] Don't provide most negative results in V2 licensing --- .../vending/licensing/LicensingService.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index f6df3b71e..1324b491b 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -166,6 +166,10 @@ public class LicensingService extends Service { // 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); @@ -188,21 +192,32 @@ public class LicensingService extends Service { Bundle bundle = new Bundle(); bundle.putString(KEY_V2_RESULT_JWT, jwt); try { - listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); + if (jwt == null) { + /* + * Suppress failures on V2. V2 is commonly used by free apps whose checker + * will not throw users out of the app if it never receives a response. + * + * This means that users who are signed in to a Google account will not + * get a worse experience in these apps than users that are not signed in. + */ + Log.i(TAG, "Suppressed negative result for package " + packageName); + } else { + listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); + } } catch (RemoteException e) { Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); e.printStackTrace(); } }, error -> { Log.e(TAG, "licenseV2 request failed with " + error.toString()); - sendError(listener, ERROR_CONTACTING_SERVER); + //sendError(listener, ERROR_CONTACTING_SERVER); – see above }); request.setShouldCache(false); queue.add(request); } catch (AuthenticatorException | IOException | OperationCanceledException e) { - sendError(listener, ERROR_CONTACTING_SERVER); + //sendError(listener, ERROR_CONTACTING_SERVER); – see above e.printStackTrace(); } }, null -- GitLab From f4dc47e8829ec82ed5a6fd9ddf70e5ceb327d312 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 3 Oct 2023 16:56:30 +0200 Subject: [PATCH 05/11] Improve license notification icon & sound --- .../LicenseServiceNotificationRunnable.java | 4 +- .../src/main/res/drawable/ic_notification.xml | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 vending-app/src/main/res/drawable/ic_notification.xml diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java index f692c413f..193c755da 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -74,7 +74,8 @@ public class LicenseServiceNotificationRunnable implements Runnable { ); Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_app_foreground) + .setSmallIcon(R.drawable.ic_notification) + .setSound(null) .setContentTitle(context.getString(R.string.license_notification_title, callerAppName)) .setContentText(context.getString(R.string.license_notification_body)) .addAction( @@ -103,6 +104,7 @@ public class LicenseServiceNotificationRunnable implements Runnable { NotificationManager.IMPORTANCE_HIGH ); channel.setDescription(context.getString(R.string.license_notification_channel_description)); + channel.setSound(null, null); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); diff --git a/vending-app/src/main/res/drawable/ic_notification.xml b/vending-app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 000000000..a1fdf972e --- /dev/null +++ b/vending-app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + -- GitLab From 2d02e7fa7dadeb386f0dc2298d5c8534d561a28c Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 13:51:02 +0200 Subject: [PATCH 06/11] Query licenses from multiple Google accounts in a row --- .../vending/licensing/LicenseChecker.java | 202 ++++++++++++++ .../vending/licensing/LicensingService.java | 259 +++++------------- 2 files changed, 271 insertions(+), 190 deletions(-) create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java new file mode 100644 index 000000000..bb8ed0273 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,202 @@ +package com.android.vending.licensing; + +import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static android.os.Binder.getCallingUid; + + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.util.Log; + +import com.android.vending.V1Container; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; + +import java.io.IOException; + +import kotlin.Unit; + +/** + * Performs license check including caller UID verification, using a given account, for which + * an auth token is fetched. + * + * @param Request parameter data value type + * @param Result type + */ +public abstract class LicenseChecker { + + private static final String TAG = "FakeLicenseChecker"; + + /* Possible response codes for checkLicense v1, from + * https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and + * the LVL library. + */ + + /** + * The application is licensed to the user. The user has purchased the application, or is authorized to + * download and install the alpha or beta version of the application. + */ + static final int LICENSED = 0x0; + /** + * The application is licensed to the user, but there is an updated application version available that is + * signed with a different key. + */ + static final int NOT_LICENSED = 0x1; + /** + * The application is not licensed to the user. + */ + static final int LICENSED_OLD_KEY = 0x2; + /** + * Server error — the application (package name) was not recognized by Google Play. + */ + static final int ERROR_NOT_MARKET_MANAGED = 0x3; + /** + * Server error — the server could not load the application's key pair for licensing. + */ + static final int ERROR_SERVER_FAILURE = 0x4; + static final int ERROR_OVER_QUOTA = 0x5; + + /** + * Local error — the Google Play application was not able to reach the licensing server, possibly because + * of network availability problems. + */ + static final int ERROR_CONTACTING_SERVER = 0x101; + /** + * Local error — the application requested a license check for a package that is not installed on the device. + */ + static final int ERROR_INVALID_PACKAGE_NAME = 0x102; + /** + * Local error — the application requested a license check for a package whose UID (package, user ID pair) + * does not match that of the requesting application. + */ + static final int ERROR_NON_MATCHING_UID = 0x103; + + static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; + + public void checkLicense(Account account, AccountManager accountManager, + String packageName, PackageManager packageManager, + RequestQueue queue, D queryData, + BiConsumerWithException onResult) + throws RemoteException { + try { + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + int versionCode = packageInfo.versionCode; + + // Verify caller identity + if (packageInfo.applicationInfo.uid != getCallingUid()) { + Log.e(TAG, + "an app illegally tried to request licenses for another app (caller: " + getCallingUid() + ")"); + onResult.accept(ERROR_NON_MATCHING_UID, null); + } else { + + accountManager.getAuthToken( + account, AUTH_TOKEN_SCOPE, false, + future -> { + try { + String auth = future.getResult().getString(KEY_AUTHTOKEN); + Request request = createRequest(packageName, auth, + versionCode, queryData, (Integer integer, R r) -> { + + try { + onResult.accept(integer, r); + } catch (RemoteException e) { + Log.e(TAG, + "After telling it the license check result, remote threw an Exception."); + e.printStackTrace(); + } + }, error -> { + Log.e(TAG, "license request failed with " + error.toString()); + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + }); + request.setShouldCache(false); + queue.add(request); + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + } + + }, null); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "an app tried to request licenses for package " + packageName + ", which does not exist"); + onResult.accept(ERROR_INVALID_PACKAGE_NAME, null); + } + } + + private static void safeSendResult( + BiConsumerWithException consumerWithException, A a, B b) { + try { + consumerWithException.accept(a, b); + } catch (Exception e) { + Log.e(TAG, "While sending result " + a + ", " + b + ", remote encountered an exception."); + e.printStackTrace(); + } + } + + public abstract Request createRequest(String packageName, String auth, int versionCode, D data, BiConsumer then, Response.ErrorListener errorListener); + + // Functional interfaces + + interface BiConsumerWithException { + void accept(A a, B b) throws T; + } + + interface BiConsumer { + void accept(A a, B b); + } + + static class Tuple { + public final A a; + public final B b; + + public Tuple(A a, B b) { + this.a = a; + this.b = b; + } + } + + // Implementations + + public static class V1 extends LicenseChecker> { + + @Override + public Request createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> then, + Response.ErrorListener errorListener) { + return new LicenseRequest.V1( + packageName, auth, versionCode, nonce, response -> { + if (response != null) { + Log.v(TAG, "licenseV1 result was " + response.result + " with signed data " + + response.signedData); + + if (response.result != null) { + then.accept(response.result, new Tuple<>(response.signedData, response.signature)); + } else { + then.accept(LICENSED, new Tuple<>(response.signedData, response.signature)); + } + } + }, errorListener + ); + } + } + + public static class V2 extends LicenseChecker { + @Override + public Request createRequest(String packageName, String auth, int versionCode, Unit data, + BiConsumer then, Response.ErrorListener errorListener) { + return new LicenseRequest.V2( + packageName, auth, versionCode, response -> { + if (response != null) { + then.accept(LICENSED, response); + } else { + then.accept(NOT_LICENSED, null); + } + }, errorListener + ); + } + } +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 1324b491b..8b58ca22e 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -5,12 +5,10 @@ package com.android.vending.licensing; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static com.android.vending.licensing.LicenseChecker.LICENSED; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.app.Service; import android.content.Intent; import android.content.pm.PackageInfo; @@ -20,14 +18,16 @@ import android.os.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,164 +44,88 @@ public class LicensingService extends Service { @Override public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); - try { - PackageManager packageManager = getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); - int versionCode = packageInfo.versionCode; - - // Verify caller identity - if (packageInfo.applicationInfo.uid != getCallingUid()) { - Log.e(TAG, "an app illegally tried to request v1 licenses for another app (caller: " + getCallingUid() + ")"); - listener.verifyLicense(ERROR_NON_MATCHING_UID, null, null); - } else { - - Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); - if (accounts.length == 0) { - Log.e(TAG, "not checking license, as user is not signed in"); - notificationRunnable.callerPackageName = packageName; - notificationRunnable.callerUid = packageInfo.applicationInfo.uid; - notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); - notificationRunnable.run(); - } else accountManager.getAuthToken( - accounts[0], AUTH_TOKEN_SCOPE, false, - future -> { - Request request = null; - try { - request = new LicenseRequest.V1( - packageName, - future.getResult().getString(KEY_AUTHTOKEN), - versionCode, nonce, data -> { - if (data != null) { - Log.v(TAG, "licenseV1 result was " + data.result + " with signed data " + - data.signedData); + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); - try { - if (data.result != null) { - listener.verifyLicense(data.result, data.signedData, data.signature); - } else { - listener.verifyLicense(LICENSED, data.signedData, data.signature); - } - } catch (RemoteException e) { - Log.e(TAG, - "After telling it the licenseV1 result, remote threw an Exception."); - e.printStackTrace(); - } - } else { - Log.v(TAG, "licenseV1 result was that user has no license"); - sendError(listener, NOT_LICENSED); - } - }, error -> { - Log.e(TAG, "licenseV1 request failed with " + error.toString()); - sendError(listener, ERROR_CONTACTING_SERVER); - }); - } catch (AuthenticatorException | IOException | OperationCanceledException e) { - sendError(listener, ERROR_CONTACTING_SERVER); - } + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicense(nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts))); + } + } - request.setShouldCache(false); - queue.add(request); - }, null); + private void checkLicense(long nonce, String packageName, PackageManager packageManager, + ILicenseResultListener listener, Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V1().checkLicense( + remainingAccounts.poll(), accountManager, packageName, packageManager, + queue, nonce, + (responseCode, stringTuple) -> { + if (responseCode != LICENSED && !remainingAccounts.isEmpty()) { + checkLicense(nonce, packageName, packageManager, listener, remainingAccounts); + } else { + listener.verifyLicense(responseCode, stringTuple.a, stringTuple.b); + } } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); - listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, null, null); - } + ); } @Override public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); - try { - PackageManager packageManager = getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); - int versionCode = packageInfo.versionCode; - - // Verify caller identity - if (packageInfo.applicationInfo.uid != getCallingUid()) { - Log.e(TAG, "an app illegally tried to request v2 licenses for another app (caller: " + getCallingUid() + ")"); - /* This negative result is provided even if users are not signed in; we expect apps - * will usually behave correctly in practise so this will not prevent users from - * using the app. - */ - listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle()); - } else { - Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); - if (accounts.length == 0) { - Log.e(TAG, "not checking license, as user is not signed in"); - notificationRunnable.callerPackageName = packageName; - notificationRunnable.callerUid = packageInfo.applicationInfo.uid; - notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); - notificationRunnable.run(); - } else accountManager.getAuthToken( - accounts[0], AUTH_TOKEN_SCOPE, false, - future -> { - try { - Bundle result = future.getResult(10, TimeUnit.SECONDS); - String auth = result.getString(KEY_AUTHTOKEN); + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicenseV2(packageName, packageManager, listener, extraParams, new LinkedList<>(Arrays.asList(accounts))); + } + } - Request request = new LicenseRequest.V2(packageName, auth, versionCode, jwt -> { - Log.v(TAG, "LicenseV2 returned JWT license value " + jwt); - Bundle bundle = new Bundle(); - bundle.putString(KEY_V2_RESULT_JWT, jwt); - try { - if (jwt == null) { - /* - * Suppress failures on V2. V2 is commonly used by free apps whose checker - * will not throw users out of the app if it never receives a response. - * - * This means that users who are signed in to a Google account will not - * get a worse experience in these apps than users that are not signed in. - */ - Log.i(TAG, "Suppressed negative result for package " + packageName); - } else { - listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); - } - } catch (RemoteException e) { - Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); - e.printStackTrace(); - } - }, error -> { - Log.e(TAG, "licenseV2 request failed with " + error.toString()); - //sendError(listener, ERROR_CONTACTING_SERVER); – see above - }); + private void checkLicenseV2(String packageName, PackageManager packageManager, + ILicenseV2ResultListener listener, Bundle extraParams, + Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V2().checkLicense( + remainingAccounts.poll(), accountManager, packageName, packageManager, queue, Unit.INSTANCE, + (responseCode, data) -> { + /* + * Suppress failures on V2. V2 is commonly used by free apps whose checker + * will not throw users out of the app if it never receives a response. + * + * This means that users who are signed in to a Google account will not + * get a worse experience in these apps than users that are not signed in. + */ + if (responseCode == LICENSED) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_V2_RESULT_JWT, data); + + listener.verifyLicense(responseCode, bundle); + } else if (!remainingAccounts.isEmpty()) { + checkLicenseV2(packageName, packageManager, listener, extraParams, remainingAccounts); + } else { + Log.i(TAG, "Suppressed negative license result for package " + packageName); + } + } + ); - request.setShouldCache(false); - queue.add(request); + } - } catch (AuthenticatorException | IOException | OperationCanceledException e) { - //sendError(listener, ERROR_CONTACTING_SERVER); – see above - e.printStackTrace(); - } - }, null - ); - } + private void handleNoAccounts(String packageName, PackageManager packageManager) { + try { + Log.e(TAG, "not checking license, as user is not signed in"); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + notificationRunnable.callerUid = packageInfo.applicationInfo.uid; + notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); - listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, new Bundle()); + Log.e(TAG, "ignored license request, but package name " + packageName + " was not known!"); + notificationRunnable.callerAppName = packageName; } - + notificationRunnable.run(); } }; - private static void sendError(ILicenseResultListener listener, int error) { - try { - listener.verifyLicense(error, null, null); - } catch (RemoteException e) { - Log.e(TAG, "After telling it that licenseV1 had an error (" + error + "), remote threw an Exception."); - } - } - - private static void sendError(ILicenseV2ResultListener listener, int error) { - try { - listener.verifyLicense(error, new Bundle()); - } catch (RemoteException e) { - Log.e(TAG, "After telling it that licenseV2 had an error (" + error + "), remote threw an Exception."); - } - } - public IBinder onBind(Intent intent) { queue = Volley.newRequestQueue(this); accountManager = AccountManager.get(this); -- GitLab From aab47dcef7836848cf04235db28646313977975d Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 13:58:05 +0200 Subject: [PATCH 07/11] Dismiss all notifications if user wants to sign in --- .../LicenseServiceNotificationRunnable.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java index 193c755da..7a82341ba 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -112,22 +112,14 @@ public class LicenseServiceNotificationRunnable implements Runnable { } - private static class Receiver extends BroadcastReceiver { + public static final class IgnoreReceiver extends BroadcastReceiver { @Override - @CallSuper public void onReceive(Context context, Intent intent) { - // Dismiss notification + + // Dismiss ignored notification NotificationManagerCompat.from(context) .cancel(intent.getIntExtra(INTENT_KEY_NOTIFICATION_ID, -1)); - } - } - - public static final class IgnoreReceiver extends Receiver { - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE); @@ -143,10 +135,12 @@ public class LicenseServiceNotificationRunnable implements Runnable { } } - public static final class SignInReceiver extends Receiver { + public static final class SignInReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); + + // Dismiss all notifications + NotificationManagerCompat.from(context).cancelAll(); Log.d(TAG, "Starting sign in activity"); Intent authIntent = new Intent(GMS_AUTH_INTENT_ACTION); -- GitLab From b8a746c956f882fe0c755fd4cc4e7da4cb7f4576 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 15:16:08 +0200 Subject: [PATCH 08/11] Always request `android.permission.GET_ACCOUNTS` --- vending-app/src/main/AndroidManifest.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index ad6dfacd8..27f5f393f 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -11,10 +11,8 @@ + - -- GitLab From 493b2342ab3199c6530fa97ef2c815bd4bcc0e73 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 15:38:45 +0200 Subject: [PATCH 09/11] Target JVM 1.8 instead of 17 in fakestore --- vending-app/build.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index ade98d48f..d48124a7d 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -44,8 +44,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = 1.8 } } -- GitLab From 03bc78cd568374352b88bb88b561a56d7d56ec23 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 12 Oct 2023 13:34:17 +0200 Subject: [PATCH 10/11] Require permission to check license --- vending-app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 27f5f393f..4763d05df 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ -- GitLab From 81ffd60867dc0dd0bf160cb70b577aabbf860bb2 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 26 Oct 2023 15:22:46 +0200 Subject: [PATCH 11/11] Apply review Reviewed-at: https://gitlab.e.foundation/e/os/GmsCore/-/merge_requests/91#note_458642 --- .../vending/licensing/LicenseChecker.java | 106 +++++++++--------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java index bb8ed0273..ff9992f7d 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -79,6 +79,9 @@ public abstract class LicenseChecker { static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; + public abstract Request createRequest(String packageName, String auth, int versionCode, D data, + BiConsumer then, Response.ErrorListener errorListener); + public void checkLicense(Account account, AccountManager accountManager, String packageName, PackageManager packageManager, RequestQueue queue, D queryData, @@ -95,25 +98,28 @@ public abstract class LicenseChecker { onResult.accept(ERROR_NON_MATCHING_UID, null); } else { + BiConsumer onRequestFinished = (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(); + } + }; + + Response.ErrorListener onRequestError = error -> { + Log.e(TAG, "license request failed with " + error.toString()); + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + }; + 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); - }); + versionCode, queryData, onRequestFinished, onRequestError); request.setShouldCache(false); queue.add(request); } catch (AuthenticatorException | IOException | OperationCanceledException e) { @@ -138,28 +144,6 @@ public abstract class LicenseChecker { } } - public abstract Request createRequest(String packageName, String auth, int versionCode, D data, BiConsumer then, Response.ErrorListener errorListener); - - // Functional interfaces - - interface BiConsumerWithException { - void accept(A a, B b) throws T; - } - - interface BiConsumer { - void accept(A a, B b); - } - - static class Tuple { - public final A a; - public final B b; - - public Tuple(A a, B b) { - this.a = a; - this.b = b; - } - } - // Implementations public static class V1 extends LicenseChecker> { @@ -169,17 +153,17 @@ public abstract class LicenseChecker { 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)); - } + 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 + } + }, errorListener ); } } @@ -190,13 +174,33 @@ public abstract class LicenseChecker { BiConsumer then, Response.ErrorListener errorListener) { return new LicenseRequest.V2( packageName, auth, versionCode, response -> { - if (response != null) { - then.accept(LICENSED, response); - } else { - then.accept(NOT_LICENSED, null); - } - }, errorListener + if (response != null) { + then.accept(LICENSED, response); + } else { + then.accept(NOT_LICENSED, null); + } + }, errorListener ); } } + + // Functional interfaces + + interface BiConsumerWithException { + void accept(A a, B b) throws T; + } + + interface BiConsumer { + void accept(A a, B b); + } + + static class Tuple { + public final A a; + public final B b; + + public Tuple(A a, B b) { + this.a = a; + this.b = b; + } + } } -- GitLab