diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 38376b97cb..f57b700b52 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" @@ -17,6 +19,25 @@ android { targetSdkVersion androidTargetSdk } + buildTypes { + debug { + postprocessing { + removeUnusedCode true + removeUnusedResources true + obfuscate false + optimizeCode true + } + } + release { + postprocessing { + removeUnusedCode true + removeUnusedResources true + obfuscate false + optimizeCode true + } + } + } + flavorDimensions = ['target'] productFlavors { "default" { @@ -34,22 +55,46 @@ android { } } + sourceSets { + main { + java { + srcDirs += "build/generated/source/proto/main/java" + } + } + } + + buildFeatures { aidl = true } lintOptions { - disable 'MissingTranslation' + disable 'MissingTranslation', 'GetLocales' } compileOptions { 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 project(':play-services-base-core') + + 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 6b255e3b4f..bfc542132b 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -9,6 +9,17 @@ android:name="com.android.vending.CHECK_LICENSE" android:protectionLevel="normal" /> + + + + + + + + + @@ -55,5 +67,11 @@ + + + + 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 0000000000..0585a28e38 --- /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/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java new file mode 100644 index 0000000000..83e76c2f07 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,211 @@ +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 not licensed to the user. + */ + static final int NOT_LICENSED = 0x1; + /** + * 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 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 abstract LicenseRequest createRequest(String packageName, String auth, int versionCode, D data, + BiConsumer then, Response.ErrorListener errorListener); + + public void checkLicense(Account account, AccountManager accountManager, String androidId, + 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 { + + 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); + LicenseRequest request = createRequest(packageName, auth, + versionCode, queryData, onRequestFinished, onRequestError); + + if (androidId != null) { + request.ANDROID_ID = Long.parseLong(androidId, 16); + } + + 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(); + } + } + + // Implementations + + public static class V1 extends LicenseChecker> { + + @Override + public LicenseRequest 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 LicenseRequest 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 + ); + } + } + + // 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; + } + } +} 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 0000000000..843716f086 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java @@ -0,0 +1,233 @@ +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 com.google.android.gms.common.BuildConfig; + +import org.microg.gms.profile.Build; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; + +import okio.ByteString; + +public abstract class LicenseRequest extends Request { + + 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; + long ANDROID_ID = 1; + private static final String FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"; + + private final Response.Listener successListener; + + + protected LicenseRequest(String url, String auth, Response.Listener successListener, Response.ErrorListener errorListener) { + super(GET, url, errorListener); + this.auth = auth; + + this.successListener = successListener; + } + + @Override + public Map getHeaders() { + + 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(Build.VERSION.SDK_INT) + .buildNumber(Build.ID) + .androidVersion(Build.VERSION.RELEASE) + .unknown(0) + .build() + ) + .unknown1(new UnknownByte12.Builder().bytes(new ByteString(new byte[]{} + )).build()) + .unknown2(1) + .build() + ) + .userAgent(new UserAgent.Builder() + .deviceName(Build.DEVICE) + .deviceHardware(Build.HARDWARE) + .deviceModelName(Build.MODEL) + .finskyVersion(FINSKY_VERSION) + .deviceProductName(Build.MODEL) + .androidId(ANDROID_ID) // must not be 0 + .buildFingerprint(Build.FINGERPRINT) + .build() + ) + .uuid(new Uuid.Builder() + .uuid(UUID.randomUUID().toString()) + .unknown(2) + .build() + ) + .build().encode(); + String xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS)); + + Log.v(TAG, "X-PS-RH: " + xPsRh); + + String userAgent = FINSKY_VERSION + " (api=3,versionCode=" + BuildConfig.VERSION_CODE + ",sdk=" + Build.VERSION.SDK + + ",device=" + encodeString(Build.DEVICE) + ",hardware=" + encodeString(Build.HARDWARE) + ",product=" + encodeString(Build.PRODUCT) + + ",platformVersionRelease=" + encodeString(Build.VERSION.RELEASE) + ",model=" + encodeString(Build.MODEL) + ",buildId=" + encodeString(Build.ID) + + ",isWideScreen=" + 0 + ",supportedAbis=" + String.join(";", Build.SUPPORTED_ABIS) + ")"; + Log.v(TAG, "User-Agent: " + userAgent); + + return Map.of( + "X-PS-RH", xPsRh, + "User-Agent", userAgent, + "Authorization", "Bearer " + auth, + "Accept-Language", "en-US", + "Connection", "Keep-Alive" + ); + } + + private static String encodeString(String s) { + return URLEncoder.encode(s).replace("+", "%20"); + } + + @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, 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, + auth, 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, String auth, int versionCode, Response.Listener successListener, + Response.ErrorListener errorListener) { + super( + "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode, + auth, 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/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java new file mode 100644 index 0000000000..84a17dacc3 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -0,0 +1,164 @@ +package com.android.vending.licensing; + +import android.Manifest; +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.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.core.app.ActivityCompat; +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_MUTABLE + ); + + String contentText = context.getString(R.string.license_notification_body); + Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setSound(null) + .setContentTitle(context.getString(R.string.license_notification_title, callerAppName)) + .setContentText(contentText) + .setStyle(new NotificationCompat.BigTextStyle().bigText(contentText)) + .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); + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED) { + 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)); + channel.setSound(null, null); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + } + + public static final class IgnoreReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + + // Dismiss ignored notification + NotificationManagerCompat.from(context) + .cancel(intent.getIntExtra(INTENT_KEY_NOTIFICATION_ID, -1)); + + 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); + if (newIgnorePackage == null) { + Log.e(TAG, "Received no ignore package; can't add to ignore list."); + return; + } + + 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 BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + + // Dismiss all notifications + NotificationManagerCompat.from(context).cancelAll(); + + 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 b479aab644..c66e9b7274 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,32 +5,227 @@ package com.android.vending.licensing; +import static com.android.vending.licensing.LicenseChecker.LICENSED; + +import android.accounts.Account; +import android.accounts.AccountManager; import android.app.Service; import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.net.Uri; +import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; + +import org.microg.gms.auth.AuthConstants; +import org.microg.gms.profile.ProfileManager; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; + +import kotlin.Unit; + public class LicensingService extends Service { private static final String TAG = "FakeLicenseService"; + private RequestQueue queue; + private AccountManager accountManager; + private LicenseServiceNotificationRunnable notificationRunnable; + private String androidId; + + private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; + + private static final Uri PROFILE_PROVIDER = Uri.parse("content://com.google.android.gms.microg.profile"); + + private static final Uri CHECKIN_SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/check-in"); + + private static final Uri SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/vending"); + + private static final String PREFERENCE_LICENSING_ENABLED = "vending_licensing"; private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { + private boolean shouldCheckLicense() { + + Cursor cursor = null; + try { + cursor = getContentResolver().query( + SETTINGS_PROVIDER, new String[]{PREFERENCE_LICENSING_ENABLED}, null, null, null + ); + + if (cursor == null || cursor.getColumnCount() != 1) { + Log.e(TAG, "settings provider not available"); + return false; + } else { + cursor.moveToNext(); + return cursor.getInt(0) != 0; + } + + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @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 + ")"); + + if (!shouldCheckLicense()) { + Log.d(TAG, "not checking license, as it is disabled by user"); + return; + } + + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); + + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicense(nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts))); + } + } + + private void checkLicense(long nonce, String packageName, PackageManager packageManager, + ILicenseResultListener listener, Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V1().checkLicense( + remainingAccounts.poll(), accountManager, androidId, 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); + } + } + ); } @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 + ")"); + + if (!shouldCheckLicense()) { + Log.d(TAG, "not checking license, as it is disabled by user"); + return; + } + + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); + + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicenseV2(packageName, packageManager, listener, extraParams, new LinkedList<>(Arrays.asList(accounts))); + } + } + + private void checkLicenseV2(String packageName, PackageManager packageManager, + ILicenseV2ResultListener listener, Bundle extraParams, + Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V2().checkLicense( + remainingAccounts.poll(), accountManager, androidId, 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); + } + } + ); + + } + + private void handleNoAccounts(String packageName, PackageManager packageManager) { + notificationRunnable.callerPackageName = packageName; + 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); + if (notificationRunnable.callerAppName == null) { + notificationRunnable.callerAppName = packageName; + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "ignored license request, but package name " + packageName + " was not known!"); + notificationRunnable.callerAppName = packageName; + } + notificationRunnable.run(); } }; public IBinder onBind(Intent intent) { + + + Cursor cursor = null; + try { + cursor = getContentResolver().query( + PROFILE_PROVIDER, null, null, null, null + ); + + if (cursor == null || cursor.getColumnCount() != 2) { + Log.e(TAG, "profile provider not available"); + } else { + Map profileData = new HashMap<>(); + while (cursor.moveToNext()) { + profileData.put(cursor.getString(0), cursor.getString(1)); + } + ProfileManager.INSTANCE.applyProfileData(profileData); + } + + } finally { + if (cursor != null) { + cursor.close(); + } + } + + try { + cursor = getContentResolver().query( + CHECKIN_SETTINGS_PROVIDER, new String[] { "androidId" }, null, null, null + ); + + if (cursor != null) { + cursor.moveToNext(); + androidId = Long.toHexString(cursor.getLong(0)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + queue = Volley.newRequestQueue(this); + accountManager = AccountManager.get(this); + notificationRunnable = new LicenseServiceNotificationRunnable(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 0000000000..dc0b71339e --- /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 deviceName = 1; // e.g. "OnePlusNord" + optional string deviceHardware = 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 deviceProductName = 5; // e.g. "OnePlusNord" + optional uint64 androidId = 6; + optional string buildFingerprint = 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 0000000000..238aa263f6 --- /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 0000000000..8e74fbcf69 --- /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 0000000000..9459ff8274 --- /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 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 0000000000..a1fdf972e1 --- /dev/null +++ b/vending-app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index 5169119862..07c309f959 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