From c42d7164cf82bd29d0ccebe1373b568c991533f8 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 16 Apr 2018 16:47:15 +0200 Subject: [PATCH 1/8] 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 Certificates and private keys + */ + 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 Certificates and private keys + */ + private QuerySelection selectNotSystemSignature(QuerySelection selection) { + if (systemSignatures == null) { + Log.i(TAG, "selectNotSystemSignature: systemSignature == null, querying for it"); + HashSet 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. *

* 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 packageInfoList = context.getPackageManager() .getInstalledPackages(PackageManager.GET_SIGNATURES); + Collections.sort(packageInfoList, new Comparator() { + @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 ""; From 21e3124b5fa8a7b189d69c11ad04d107edc9911d Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 17 Apr 2018 14:32:10 +0200 Subject: [PATCH 2/8] prevent crash when starting swap on devices without Bluetooth/WiFi This was introduced in f90b030e76ddb03e00bc0d46977c01c1bae3936d --- .../main/java/org/fdroid/fdroid/localrepo/SwapService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index b7e509179..e0b916096 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -537,11 +537,11 @@ public class SwapService extends Service { Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); - if (!SwapService.wasBluetoothEnabledBeforeSwap()) { + if (bluetoothAdapter != null && !wasBluetoothEnabledBeforeSwap()) { bluetoothAdapter.disable(); } - if (!SwapService.wasWifiEnabledBeforeSwap()) { + if (wifiManager != null && !wasWifiEnabledBeforeSwap()) { wifiManager.setWifiEnabled(false); } From eb77f72cd29ce771de0c9932608a78cbf0ff1c3f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 17 Apr 2018 22:43:11 +0200 Subject: [PATCH 3/8] store last working mirror per repo For mirroring to work on multiple repos, this must be stored and used per- repo. The timeout and number of tries seem fine to keep global to reduce the total amount of mirror churn when this logic is searching. --- app/src/main/java/org/fdroid/fdroid/FDroidApp.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index fc16184b8..d6321dfbb 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -36,6 +36,7 @@ import android.graphics.Bitmap; import android.os.Build; import android.os.Environment; import android.os.StrictMode; +import android.support.v4.util.LongSparseArray; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -113,7 +114,7 @@ public class FDroidApp extends Application { public static volatile int networkState = ConnectivityMonitorService.FLAG_NET_UNAVAILABLE; - private static volatile String lastWorkingMirror = null; + private static volatile LongSparseArray lastWorkingMirrorArray = new LongSparseArray<>(1); private static volatile int numTries = Integer.MAX_VALUE; private static volatile int timeout = 10000; @@ -242,6 +243,7 @@ public class FDroidApp extends Application { public static String getMirror(String urlString, Repo repo2) throws IOException { if (repo2.hasMirrors()) { + String lastWorkingMirror = lastWorkingMirrorArray.get(repo2.getId()); if (lastWorkingMirror == null) { lastWorkingMirror = repo2.address; } @@ -264,7 +266,7 @@ public class FDroidApp extends Application { String newUrl = urlString.replace(lastWorkingMirror, mirror); Utils.debugLog(TAG, "Trying mirror " + mirror + " after " + lastWorkingMirror + " failed," + " timeout=" + timeout / 1000 + "s"); - lastWorkingMirror = mirror; + lastWorkingMirrorArray.put(repo2.getId(), mirror); numTries--; return newUrl; } else { @@ -278,7 +280,9 @@ public class FDroidApp extends Application { public static void resetMirrorVars() { // Reset last working mirror, numtries, and timeout - lastWorkingMirror = null; + for (int i = 0; i < lastWorkingMirrorArray.size(); i++) { + lastWorkingMirrorArray.removeAt(i); + } numTries = Integer.MAX_VALUE; timeout = 10000; } From 3fd1b055b3aeaee8a0320f58da499c991b63f944 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 Apr 2018 10:01:31 +0200 Subject: [PATCH 4/8] tame debug logging in CompatibilityChecker It makes a huge dump on every index refresh, making troubleshooting other things around the index hard. --- .../fdroid/fdroid/CompatibilityChecker.java | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java b/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java index 0c007a38d..1cd3db664 100644 --- a/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java +++ b/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java @@ -7,8 +7,6 @@ import android.content.pm.PackageManager; import android.os.Build; import android.preference.PreferenceManager; import android.support.annotation.Nullable; -import android.text.TextUtils; - import org.fdroid.fdroid.compat.SupportedArchitectures; import org.fdroid.fdroid.data.Apk; @@ -22,12 +20,11 @@ import java.util.Set; // find reasons why an apk may be incompatible with the user's device. public class CompatibilityChecker { - private static final String TAG = "Compatibility"; + public static final String TAG = "Compatibility"; private final Context context; private final Set features; private final String[] cpuAbis; - private final String cpuAbisDesc; private final boolean forceTouchApps; public CompatibilityChecker(Context ctx) { @@ -43,13 +40,6 @@ public class CompatibilityChecker { if (pm != null) { final FeatureInfo[] featureArray = pm.getSystemAvailableFeatures(); if (featureArray != null) { - if (BuildConfig.DEBUG) { - StringBuilder logMsg = new StringBuilder("Available device features:"); - for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { - logMsg.append('\n').append(fi.name); - } - Utils.debugLog(TAG, logMsg.toString()); - } for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { features.add(fi.name); } @@ -57,18 +47,6 @@ public class CompatibilityChecker { } cpuAbis = SupportedArchitectures.getAbis(); - - StringBuilder builder = new StringBuilder(); - boolean first = true; - for (final String abi : cpuAbis) { - if (first) { - first = false; - } else { - builder.append(", "); - } - builder.append(abi); - } - cpuAbisDesc = builder.toString(); } private boolean compatibleApi(@Nullable String[] nativecode) { @@ -107,16 +85,11 @@ public class CompatibilityChecker { } if (!features.contains(feat)) { Collections.addAll(incompatibleReasons, feat.split(",")); - Utils.debugLog(TAG, apk.packageName + " vercode " + apk.versionCode - + " is incompatible based on lack of " + feat); } } } if (!compatibleApi(apk.nativecode)) { Collections.addAll(incompatibleReasons, apk.nativecode); - Utils.debugLog(TAG, apk.packageName + " vercode " + apk.versionCode - + " only supports " + TextUtils.join(", ", apk.nativecode) - + " while your architectures are " + cpuAbisDesc); } return incompatibleReasons; From b9c247e2b12660dd0c38def9ba00141930b0e7d1 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 Apr 2018 10:50:09 +0200 Subject: [PATCH 5/8] if WifiManager fails to return netmask, directly query net interfaces Google broke returning the netmask somewhere around android-21. This could be done using more official APIs, but this reuses stuff that needs to be there anyway. closes #577 https://code.google.com/p/android/issues/detail?id=82477#c5 https://issuetracker.google.com/issues/37015180 --- app/src/main/java/org/fdroid/fdroid/FDroidApp.java | 4 +++- .../fdroid/fdroid/net/WifiStateChangeService.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index d6321dfbb..c68f6737c 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -114,6 +114,8 @@ public class FDroidApp extends Application { public static volatile int networkState = ConnectivityMonitorService.FLAG_NET_UNAVAILABLE; + public static final SubnetUtils.SubnetInfo UNSET_SUBNET_INFO = new SubnetUtils("0.0.0.0/32").getInfo(); + private static volatile LongSparseArray lastWorkingMirrorArray = new LongSparseArray<>(1); private static volatile int numTries = Integer.MAX_VALUE; private static volatile int timeout = 10000; @@ -231,7 +233,7 @@ public class FDroidApp extends Application { public static void initWifiSettings() { port = 8888; ipAddressString = null; - subnetInfo = new SubnetUtils("0.0.0.0/32").getInfo(); + subnetInfo = UNSET_SUBNET_INFO; ssid = ""; bssid = ""; repo = new Repo(); diff --git a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java index fcd62ad6c..d91100ea4 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -121,6 +121,10 @@ public class WifiStateChangeService extends IntentService { } } } + if (FDroidApp.ipAddressString == null + || FDroidApp.subnetInfo == FDroidApp.UNSET_SUBNET_INFO) { + setIpInfoFromNetworkInterface(); + } } else if (wifiState == WifiManager.WIFI_STATE_DISABLED || wifiState == WifiManager.WIFI_STATE_DISABLING) { // try once to see if its a hotspot @@ -210,6 +214,16 @@ public class WifiStateChangeService extends IntentService { } } + /** + * Search for known Wi-Fi, Hotspot, and local network interfaces and get + * the IP Address info from it. This is necessary because network + * interfaces in Hotspot/AP mode do not show up in the regular + * {@link WifiManager} queries, and also on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP Android 5.0} and newer, + * {@link WifiManager#getDhcpInfo()} returns an invalid netmask. + * + * @see netmask of WifiManager.getDhcpInfo() is always zero on Android 5.0 + */ private void setIpInfoFromNetworkInterface() { try { Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); From fa1331139f5c87f9d3d188d1a72ede901571f1ea Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 Apr 2018 12:09:31 +0200 Subject: [PATCH 6/8] delete all swap repos when before swap starts and after it stops For now, swap repos are only trusted as long as swapping is active. They should have a long lived trust based on the signing key, but that requires that the repos are stored in the database by fingerprint, not by URL address. #295 #703 --- .../fdroid/fdroid/localrepo/SwapService.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index e0b916096..3a16ea723 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -481,6 +481,8 @@ public class SwapService extends Service { Utils.debugLog(TAG, "Creating swap service."); startForeground(NOTIFICATION, createNotification()); + deleteAllSwapRepos(); + CacheSwapAppsService.startCaching(this); swapPreferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); @@ -553,6 +555,8 @@ public class SwapService extends Service { } stopForeground(true); + deleteAllSwapRepos(); + super.onDestroy(); } @@ -568,7 +572,26 @@ public class SwapService extends Service { .build(); } + /** + * For now, swap repos are only trusted as long as swapping is active. They + * should have a long lived trust based on the signing key, but that requires + * that the repos are stored in the database by fingerprint, not by URL address. + * + * @see TOFU in swap + * @see + * signing key fingerprint should be sole ID for repos in the database + */ + private void deleteAllSwapRepos() { + for (Repo repo : RepoProvider.Helper.all(this)) { + if (repo.isSwap) { + Utils.debugLog(TAG, "Removing stale swap repo: " + repo.address + " - " + repo.fingerprint); + RepoProvider.Helper.remove(this, repo.getId()); + } + } + } + private void initTimer() { + // TODO replace by Android scheduler if (timer != null) { Utils.debugLog(TAG, "Cancelling existing timeout timer so timeout can be reset."); timer.cancel(); From 045fc1a35ef4137f67bdb57673349a9c0d472e26 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 Apr 2018 12:15:15 +0200 Subject: [PATCH 7/8] make "Scan QR" immediately show the QR screen, not "Select Apps" --- .../java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 6098a58cd..5d93b8e72 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -425,7 +425,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { }) .create().show(); } else { - showSelectApps(); + showWifiQr(); } } From 2615c461f20a0f6de11fefdea62ee907c80f74e0 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 Apr 2018 16:58:51 +0200 Subject: [PATCH 8/8] debounce incoming "WiFi Connected" events for reliable status Some devices send multiple copies of given events, like a Moto G often sends three {@code CONNECTED} events. So they have to be debounced to keep the {@link #BROADCAST} useful. --- .../fdroid/net/WifiStateChangeService.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java index d91100ea4..c4280c3cc 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -41,6 +41,10 @@ import java.util.Locale; * the current state because it means that something about the wifi has * changed. Having the {@code Thread} also makes it easy to kill work * that is in progress. + *

+ * Some devices send multiple copies of given events, like a Moto G often + * sends three {@code CONNECTED} events. So they have to be debounced to + * keep the {@link #BROADCAST} useful. */ @SuppressWarnings("LineLength") public class WifiStateChangeService extends IntentService { @@ -50,6 +54,7 @@ public class WifiStateChangeService extends IntentService { private WifiManager wifiManager; private static WifiInfoThread wifiInfoThread; + private static int previousWifiState = Integer.MIN_VALUE; public WifiStateChangeService() { super("WifiStateChangeService"); @@ -70,16 +75,17 @@ public class WifiStateChangeService extends IntentService { Utils.debugLog(TAG, "received null Intent, ignoring"); return; } - Utils.debugLog(TAG, "WiFi change service started, clearing info about wifi state until we have figured it out again."); + Utils.debugLog(TAG, "WiFi change service started."); NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE); int wifiState = wifiManager.getWifiState(); if (ni == null || ni.isConnected()) { Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState)); - if (wifiState == WifiManager.WIFI_STATE_ENABLED - || wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot - || wifiState == WifiManager.WIFI_STATE_DISABLED // might be hotspot - || wifiState == WifiManager.WIFI_STATE_UNKNOWN) { // might be hotspot + if (previousWifiState != wifiState && + (wifiState == WifiManager.WIFI_STATE_ENABLED + || wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot + || wifiState == WifiManager.WIFI_STATE_DISABLED // might be hotspot + || wifiState == WifiManager.WIFI_STATE_UNKNOWN)) { // might be hotspot if (wifiInfoThread != null) { wifiInfoThread.interrupt(); } @@ -287,7 +293,10 @@ public class WifiStateChangeService extends IntentService { return "WIFI_STATE_ENABLED"; case WifiManager.WIFI_STATE_UNKNOWN: return "WIFI_STATE_UNKNOWN"; + case Integer.MIN_VALUE: + return "previous value unset"; + default: + return "~not mapped~"; } - return null; } }