From c42d7164cf82bd29d0ccebe1373b568c991533f8 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner <hans@eds.org>
Date: Mon, 16 Apr 2018 16:47:15 +0200
Subject: [PATCH] exclude ROM apps from default swap app listing

Apps that are built as part of the ROM and signed by the platform keys
should very rarely be swapped.  This removes them from the default
list by comparing the signing keys.

This filter is deliberately only included on the list function and not on
the search function.  If people want to share system apps, they'll be able
to find them with the search function, but the system apps won't show up
by default.

https://source.android.com/devices/tech/ota/sign_builds#certificates-keys

closes #440
---
 .../fdroid/data/InstalledAppProvider.java     | 46 +++++++++++++++++++
 .../data/InstalledAppProviderService.java     | 18 +++++++-
 2 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java
index 37a6a07d6..5da337996 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java
@@ -18,6 +18,7 @@ import org.fdroid.fdroid.data.Schema.InstalledAppTable;
 import org.fdroid.fdroid.data.Schema.InstalledAppTable.Cols;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 
 public class InstalledAppProvider extends FDroidProvider {
@@ -81,6 +82,20 @@ public class InstalledAppProvider extends FDroidProvider {
 
     private static final UriMatcher MATCHER = new UriMatcher(-1);
 
+    /**
+     * Built-in apps that are signed by the various Android ROM keys.
+     *
+     * @see <a href="https://source.android.com/devices/tech/ota/sign_builds#certificates-keys">Certificates and private keys</a>
+     */
+    private static final String[] SYSTEM_PACKAGES = {
+            "android", // platform key
+            "com.android.email", // test/release key
+            "com.android.contacts", // shared key
+            "com.android.providers.downloads", // media key
+    };
+
+    private static String[] systemSignatures;
+
     static {
         MATCHER.addURI(getAuthority(), null, CODE_LIST);
         MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", CODE_SEARCH);
@@ -117,6 +132,36 @@ public class InstalledAppProvider extends FDroidProvider {
         return packageName; // all else fails, return packageName
     }
 
+    /**
+     * Add SQL selection statement to exclude {@link InstalledApp}s that were
+     * signed by the platform/shared/media/testkey keys.
+     *
+     * @see <a href="https://source.android.com/devices/tech/ota/sign_builds#certificates-keys">Certificates and private keys</a>
+     */
+    private QuerySelection selectNotSystemSignature(QuerySelection selection) {
+        if (systemSignatures == null) {
+            Log.i(TAG, "selectNotSystemSignature: systemSignature == null, querying for it");
+            HashSet<String> signatures = new HashSet<>();
+            for (String packageName : SYSTEM_PACKAGES) {
+                Cursor cursor = query(InstalledAppProvider.getAppUri(packageName), new String[]{Cols.SIGNATURE},
+                        null, null, null);
+                if (cursor != null) {
+                    if (cursor.moveToFirst()) {
+                        signatures.add(cursor.getString(cursor.getColumnIndex(Cols.SIGNATURE)));
+                    }
+                    cursor.close();
+                }
+            }
+            systemSignatures = signatures.toArray(new String[signatures.size()]);
+        }
+
+        Log.i(TAG, "excluding InstalledApps signed by system signatures");
+        for (String systemSignature : systemSignatures) {
+            selection = selection.add("NOT " + Cols.SIGNATURE + " IN (?)", new String[]{systemSignature});
+        }
+        return selection;
+    }
+
     @Override
     protected String getTableName() {
         return InstalledAppTable.NAME;
@@ -185,6 +230,7 @@ public class InstalledAppProvider extends FDroidProvider {
         QuerySelection selection = new QuerySelection(customSelection, selectionArgs);
         switch (MATCHER.match(uri)) {
             case CODE_LIST:
+                selection = selectNotSystemSignature(selection);
                 break;
 
             case CODE_SINGLE:
diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java
index 1b531d205..2df49e74d 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java
@@ -23,6 +23,8 @@ import rx.subjects.PublishSubject;
 import java.io.File;
 import java.io.FilenameFilter;
 import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -146,7 +148,10 @@ public class InstalledAppProviderService extends IntentService {
      * Make sure that {@link InstalledAppProvider}, our database of installed apps,
      * is in sync with what the {@link PackageManager} tells us is installed. Once
      * completed, the relevant {@link android.content.ContentProvider}s will be
-     * notified of any changes to installed statuses.
+     * notified of any changes to installed statuses.  The packages are processed
+     * in alphabetically order so that "{@code android}" is processed first.  That
+     * is always present and signed by the system key, so it is the source of the
+     * system key for comparing all packages.
      * <p>
      * The installed app cache could get out of sync, e.g. if F-Droid crashed/ or
      * ran out of battery half way through responding to {@link Intent#ACTION_PACKAGE_ADDED}.
@@ -169,6 +174,12 @@ public class InstalledAppProviderService extends IntentService {
 
         List<PackageInfo> packageInfoList = context.getPackageManager()
                 .getInstalledPackages(PackageManager.GET_SIGNATURES);
+        Collections.sort(packageInfoList, new Comparator<PackageInfo>() {
+            @Override
+            public int compare(PackageInfo o1, PackageInfo o2) {
+                return o1.packageName.compareTo(o2.packageName);
+            }
+        });
         for (PackageInfo packageInfo : packageInfoList) {
             if (cachedInfo.containsKey(packageInfo.packageName)) {
                 if (packageInfo.lastUpdateTime < 1262300400000L // 2010-01-01 00:00
@@ -314,6 +325,11 @@ public class InstalledAppProviderService extends IntentService {
         context.getContentResolver().delete(uri, null, null);
     }
 
+    /**
+     * Get the fingerprint used to represent an APK signing key in F-Droid.
+     * This is a custom fingerprint algorithm that was kind of accidentally
+     * created, but is still in use.
+     */
     private static String getPackageSig(PackageInfo info) {
         if (info == null || info.signatures == null || info.signatures.length < 1) {
             return "";